|
| 1 | +#include <common/bip352.h> |
| 2 | +#include <span.h> |
| 3 | +#include <addresstype.h> |
| 4 | +#include <policy/policy.h> |
| 5 | +#include <script/solver.h> |
| 6 | +#include <test/data/bip352_send_and_receive_vectors.json.h> |
| 7 | + |
| 8 | +#include <test/util/setup_common.h> |
| 9 | +#include <hash.h> |
| 10 | + |
| 11 | +#include <boost/test/unit_test.hpp> |
| 12 | +#include <test/util/json.h> |
| 13 | +#include <vector> |
| 14 | +#include <util/bip32.h> |
| 15 | +#include <util/strencodings.h> |
| 16 | +#include <key_io.h> |
| 17 | +#include <streams.h> |
| 18 | + |
| 19 | +namespace wallet { |
| 20 | +BOOST_FIXTURE_TEST_SUITE(bip352_tests, BasicTestingSetup) |
| 21 | + |
| 22 | +CKey ParseHexToCKey(std::string hex) { |
| 23 | + CKey output; |
| 24 | + std::vector<unsigned char> hex_data = ParseHex(hex); |
| 25 | + output.Set(hex_data.begin(), hex_data.end(), true); |
| 26 | + return output; |
| 27 | +}; |
| 28 | + |
| 29 | +CKey GetKeyFromBIP32Path(std::vector<std::byte> seed, std::vector<uint32_t> path) |
| 30 | +{ |
| 31 | + CExtKey key; |
| 32 | + key.SetSeed(seed); |
| 33 | + for (auto index : path) { |
| 34 | + BOOST_CHECK(key.Derive(key, index)); |
| 35 | + } |
| 36 | + return key.key; |
| 37 | +} |
| 38 | + |
| 39 | +BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors) |
| 40 | +{ |
| 41 | + UniValue tests; |
| 42 | + tests.read(json_tests::bip352_send_and_receive_vectors); |
| 43 | + |
| 44 | + for (const auto& vec : tests.getValues()) { |
| 45 | + // run sending tests |
| 46 | + BOOST_TEST_MESSAGE(vec["comment"].get_str()); |
| 47 | + for (const auto& sender : vec["sending"].getValues()) { |
| 48 | + const UniValue& given = sender["given"]; |
| 49 | + const UniValue& expected = sender["expected"]; |
| 50 | + |
| 51 | + std::vector<COutPoint> outpoints; |
| 52 | + std::vector<CKey> keys; |
| 53 | + std::vector<KeyPair> taproot_keys; |
| 54 | + for (const auto& input : given["vin"].getValues()) { |
| 55 | + COutPoint outpoint{Txid::FromHex(input["txid"].get_str()).value(), input["vout"].getInt<uint32_t>()}; |
| 56 | + outpoints.push_back(outpoint); |
| 57 | + const auto& spk_bytes = ParseHex(input["prevout"]["scriptPubKey"]["hex"].get_str()); |
| 58 | + CScript spk = CScript(spk_bytes.begin(), spk_bytes.end()); |
| 59 | + const auto& script_sig_bytes = ParseHex(input["scriptSig"].get_str()); |
| 60 | + CScript script_sig = CScript(script_sig_bytes.begin(), script_sig_bytes.end()); |
| 61 | + CTxIn txin{outpoint, script_sig}; |
| 62 | + CScriptWitness witness; |
| 63 | + // read the field txWitness as a stream and write txWitness >> witness.stack; |
| 64 | + auto witness_str = ParseHex(input["txinwitness"].get_str()); |
| 65 | + if (!witness_str.empty()) { |
| 66 | + SpanReader(witness_str) >> witness.stack; |
| 67 | + txin.scriptWitness = witness; |
| 68 | + } |
| 69 | + |
| 70 | + // check if this is a silent payment input by trying to extract the public key |
| 71 | + const auto& pubkey = bip352::GetPubKeyFromInput(txin, spk); |
| 72 | + if (pubkey.has_value()) { |
| 73 | + std::vector<std::vector<unsigned char>> solutions; |
| 74 | + TxoutType type = Solver(spk, solutions); |
| 75 | + if (type == TxoutType::WITNESS_V1_TAPROOT) { |
| 76 | + taproot_keys.emplace_back(ParseHexToCKey(input["private_key"].get_str()).ComputeKeyPair(nullptr)); |
| 77 | + } else { |
| 78 | + keys.emplace_back(ParseHexToCKey(input["private_key"].get_str())); |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + if (taproot_keys.empty() && keys.empty()) continue; |
| 83 | + // silent payments logic |
| 84 | + auto smallest_outpoint = std::min_element(outpoints.begin(), outpoints.end(), bip352::BIP352Comparator()); |
| 85 | + std::map<size_t, V0SilentPaymentDestination> sp_dests; |
| 86 | + const std::vector<UniValue>& silent_payment_addresses = given["recipients"].getValues(); |
| 87 | + for (size_t i = 0; i < silent_payment_addresses.size(); ++i) { |
| 88 | + const CTxDestination& tx_dest = DecodeDestination(silent_payment_addresses[i].get_str()); |
| 89 | + if (const auto* sp = std::get_if<V0SilentPaymentDestination>(&tx_dest)) { |
| 90 | + sp_dests[i] = *sp; |
| 91 | + } |
| 92 | + } |
| 93 | + auto sp_tr_dests = bip352::GenerateSilentPaymentTaprootDestinations(sp_dests, keys, taproot_keys, *smallest_outpoint); |
| 94 | + // This means the inputs summed to zero, which realistically would only happen maliciously. In this case, just move on |
| 95 | + if (!sp_tr_dests.has_value()) continue; |
| 96 | + bool match = false; |
| 97 | + for (const auto& candidate_set : expected["outputs"].getValues()) { |
| 98 | + BOOST_CHECK(sp_tr_dests->size() == candidate_set.size()); |
| 99 | + std::vector<WitnessV1Taproot> expected_spks; |
| 100 | + for (const auto& output : candidate_set.getValues()) { |
| 101 | + const WitnessV1Taproot tap{XOnlyPubKey(ParseHex(output.get_str()))}; |
| 102 | + expected_spks.push_back(tap); |
| 103 | + } |
| 104 | + match = true; |
| 105 | + for (const auto& [_, spk]: *sp_tr_dests) { |
| 106 | + if (std::find(expected_spks.begin(), expected_spks.end(), spk) == expected_spks.end()) { |
| 107 | + match = false; |
| 108 | + break; |
| 109 | + } |
| 110 | + } |
| 111 | + if (!match) { |
| 112 | + continue; |
| 113 | + } else { |
| 114 | + break; |
| 115 | + } |
| 116 | + } |
| 117 | + BOOST_CHECK(match); |
| 118 | + } |
| 119 | + |
| 120 | + // Test receiving |
| 121 | + for (const auto& recipient : vec["receiving"].getValues()) { |
| 122 | + |
| 123 | + const UniValue& given = recipient["given"]; |
| 124 | + const UniValue& expected = recipient["expected"]; |
| 125 | + |
| 126 | + std::vector<CTxIn> vin; |
| 127 | + std::map<COutPoint, Coin> coins; |
| 128 | + for (const auto& input : given["vin"].getValues()) { |
| 129 | + COutPoint outpoint{Txid::FromHex(input["txid"].get_str()).value(), input["vout"].getInt<uint32_t>()}; |
| 130 | + const auto& spk_bytes = ParseHex(input["prevout"]["scriptPubKey"]["hex"].get_str()); |
| 131 | + CScript spk = CScript(spk_bytes.begin(), spk_bytes.end()); |
| 132 | + const auto& script_sig_bytes = ParseHex(input["scriptSig"].get_str()); |
| 133 | + CScript script_sig = CScript(script_sig_bytes.begin(), script_sig_bytes.end()); |
| 134 | + CTxIn txin{outpoint, script_sig}; |
| 135 | + CScriptWitness witness; |
| 136 | + // read the field txWitness as a stream and write txWitness >> witness.stack; |
| 137 | + auto witness_str = ParseHex(input["txinwitness"].get_str()); |
| 138 | + if (!witness_str.empty()) { |
| 139 | + SpanReader(witness_str) >> witness.stack; |
| 140 | + txin.scriptWitness = witness; |
| 141 | + } |
| 142 | + vin.push_back(txin); |
| 143 | + coins[outpoint] = Coin{CTxOut{{}, spk}, 0, false}; |
| 144 | + } |
| 145 | + auto pub_tweak_data = bip352::GetSilentPaymentsPublicData(vin, coins); |
| 146 | + // If we don't get any tweak data from the transaction inputs, it is not a silent payment |
| 147 | + // transaction, so we skip it. |
| 148 | + if (!pub_tweak_data.has_value()) continue; |
| 149 | + std::vector<XOnlyPubKey> output_pub_keys; |
| 150 | + for (const auto& pubkey : given["outputs"].getValues()) { |
| 151 | + output_pub_keys.emplace_back(ParseHex(pubkey.get_str())); |
| 152 | + } |
| 153 | + |
| 154 | + CKey scan_priv_key = ParseHexToCKey(given["key_material"]["scan_priv_key"].get_str()); |
| 155 | + CKey spend_priv_key = ParseHexToCKey(given["key_material"]["spend_priv_key"].get_str()); |
| 156 | + V0SilentPaymentDestination sp_address{scan_priv_key.GetPubKey(), spend_priv_key.GetPubKey()}; |
| 157 | + |
| 158 | + std::map<CPubKey, uint256> labels; |
| 159 | + // Always calculate the change key, whether we use labels or not |
| 160 | + const auto& [change_pubkey, change_tweak] = bip352::CreateLabelTweak(scan_priv_key, 0); |
| 161 | + labels[change_pubkey] = change_tweak; |
| 162 | + // If labels are used, add them to the dictionary |
| 163 | + for (const auto& label : given["labels"].getValues()) { |
| 164 | + const auto& [label_pubkey, label_tweak] = bip352::CreateLabelTweak(scan_priv_key, label.getInt<int>()); |
| 165 | + labels[label_pubkey] = label_tweak; |
| 166 | + } |
| 167 | + |
| 168 | + |
| 169 | + // Scanning |
| 170 | + const auto& found_outputs = bip352::ScanForSilentPaymentOutputs(scan_priv_key, *pub_tweak_data, sp_address.m_spend_pubkey, output_pub_keys, labels); |
| 171 | + // The transaction may be a silent payment transaction, but it does not contain any outputs for us, |
| 172 | + // so we continue to the next transaction. |
| 173 | + if (!found_outputs.has_value()) continue; |
| 174 | + std::vector<XOnlyPubKey> expected_outputs; |
| 175 | + for (const auto& output : expected["outputs"].getValues()) { |
| 176 | + std::string pubkey_hex = output["pub_key"].get_str(); |
| 177 | + expected_outputs.emplace_back(ParseHex(pubkey_hex)); |
| 178 | + } |
| 179 | + BOOST_TEST_MESSAGE(found_outputs->size()); |
| 180 | + BOOST_CHECK(found_outputs->size() == expected_outputs.size()); |
| 181 | + for (const auto& output : *found_outputs) { |
| 182 | + BOOST_CHECK(std::find(expected_outputs.begin(), expected_outputs.end(), output.output) != expected_outputs.end()); |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | +} |
| 187 | +BOOST_AUTO_TEST_SUITE_END() |
| 188 | +} // namespace wallet |
0 commit comments