Skip to content

Commit

Permalink
Add BIP352 test vectors as unit tests
Browse files Browse the repository at this point in the history
Use the test vectors to test sending and receiving. A few cases are not
covered here, namely anything that requires testing specific to the
wallet. For example:

* Taproot script path spending is not tested, as that is better tested in
  a wallets coin selection / signing logic
* Re-computing outputs during RBF is not tested, as that is better
  tested in a wallets RBF logic

The unit tests are written in such a way that adding new test cases is
as easy as updating the JSON file.
  • Loading branch information
josibake committed Apr 27, 2024
1 parent 66995bd commit 6f238b7
Show file tree
Hide file tree
Showing 4 changed files with 2,950 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Makefile.test.include
Expand Up @@ -17,6 +17,7 @@ FUZZ_BINARY=test/fuzz/fuzz$(EXEEXT)
JSON_TEST_FILES = \
test/data/script_tests.json \
test/data/bip341_wallet_vectors.json \
test/data/bip352_send_and_receive_vectors.json \
test/data/base58_encode_decode.json \
test/data/blockfilters.json \
test/data/key_io_valid.json \
Expand Down Expand Up @@ -75,6 +76,7 @@ BITCOIN_TESTS =\
test/bech32_tests.cpp \
test/bip32_tests.cpp \
test/bip324_tests.cpp \
test/bip352_tests.cpp \
test/blockchain_tests.cpp \
test/blockencodings_tests.cpp \
test/blockfilter_index_tests.cpp \
Expand Down
173 changes: 173 additions & 0 deletions src/test/bip352_tests.cpp
@@ -0,0 +1,173 @@
#include <bip352.h>
#include <span.h>
#include <addresstype.h>
#include <policy/policy.h>
#include <script/solver.h>
#include <test/data/bip352_send_and_receive_vectors.json.h>

#include <test/util/setup_common.h>
#include <hash.h>

#include <boost/test/unit_test.hpp>
#include <test/util/json.h>
#include <vector>
#include <util/bip32.h>
#include <util/strencodings.h>
#include <key_io.h>
#include <streams.h>

namespace wallet {
BOOST_FIXTURE_TEST_SUITE(bip352_tests, BasicTestingSetup)

CKey ParseHexToCKey(std::string hex) {
CKey output;
std::vector<unsigned char> hex_data = ParseHex(hex);
output.Set(hex_data.begin(), hex_data.end(), true);
return output;
};

CKey GetKeyFromBIP32Path(std::vector<std::byte> seed, std::vector<uint32_t> path)
{
CExtKey key;
key.SetSeed(seed);
for (auto index : path) {
BOOST_CHECK(key.Derive(key, index));
}
return key.key;
}

BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)
{
UniValue tests;
tests.read(json_tests::bip352_send_and_receive_vectors);

for (const auto& vec : tests.getValues()) {
// run sending tests
BOOST_TEST_MESSAGE(vec["comment"].get_str());
for (const auto& sender : vec["sending"].getValues()) {
const UniValue& given = sender["given"];
const UniValue& expected = sender["expected"];

std::vector<COutPoint> outpoints;
std::vector<CKey> keys;
std::vector<CKey> taproot_keys;
for (const auto& input : given["vin"].getValues()) {
COutPoint outpoint{TxidFromString(input["txid"].get_str()), input["vout"].getInt<uint32_t>()};
outpoints.push_back(outpoint);
const auto& spk_bytes = ParseHex(input["prevout"]["scriptPubKey"]["hex"].get_str());
CScript spk = CScript(spk_bytes.begin(), spk_bytes.end());
const auto& script_sig_bytes = ParseHex(input["scriptSig"].get_str());
CScript script_sig = CScript(script_sig_bytes.begin(), script_sig_bytes.end());
CTxIn txin{outpoint, script_sig};
CScriptWitness witness;
// read the field txWitness as a stream and write txWitness >> witness.stack;
auto witness_str = ParseHex(input["txinwitness"].get_str());
if (!witness_str.empty()) {
SpanReader(witness_str) >> witness.stack;
txin.scriptWitness = witness;
}

// check if this is a silent payment input by trying to extract the public key
const auto& pubkey = BIP352::GetPubKeyFromInput(txin, spk);
if (pubkey.has_value()) {
std::vector<std::vector<unsigned char>> solutions;
TxoutType type = Solver(spk, solutions);
if (type == TxoutType::WITNESS_V1_TAPROOT) {
taproot_keys.emplace_back(ParseHexToCKey(input["private_key"].get_str()));
} else {
keys.emplace_back(ParseHexToCKey(input["private_key"].get_str()));
}
}
}
if (taproot_keys.empty() && keys.empty()) continue;
// silent payments logic
auto smallest_outpoint = std::min_element(outpoints.begin(), outpoints.end());
std::map<size_t, V0SilentPaymentDestination> sp_dests;
const std::vector<UniValue>& silent_payment_addresses = given["recipients"].getValues();
for (size_t i = 0; i < silent_payment_addresses.size(); ++i) {
const CTxDestination& tx_dest = DecodeDestination(silent_payment_addresses[i].get_str());
if (const auto* sp = std::get_if<V0SilentPaymentDestination>(&tx_dest)) {
sp_dests[i] = *sp;
}
}
std::map<size_t, WitnessV1Taproot> sp_tr_dests = BIP352::GenerateSilentPaymentTaprootDestinations(sp_dests, keys, taproot_keys, *smallest_outpoint);
std::vector<WitnessV1Taproot> expected_spks;
for (const auto& recipient : expected["outputs"].getValues()) {
const WitnessV1Taproot tap{XOnlyPubKey(ParseHex(recipient.get_str()))};
expected_spks.push_back(tap);
}
BOOST_CHECK(sp_tr_dests.size() == expected["n_outputs"].getInt<size_t>());
for (const auto& [_, spk]: sp_tr_dests) {
BOOST_CHECK(std::find(expected_spks.begin(), expected_spks.end(), spk) != expected_spks.end());
}
}

// Test receiving
for (const auto& recipient : vec["receiving"].getValues()) {

const UniValue& given = recipient["given"];
const UniValue& expected = recipient["expected"];

std::vector<CTxIn> vin;
std::map<COutPoint, Coin> coins;
for (const auto& input : given["vin"].getValues()) {
COutPoint outpoint{TxidFromString(input["txid"].get_str()), input["vout"].getInt<uint32_t>()};
const auto& spk_bytes = ParseHex(input["prevout"]["scriptPubKey"]["hex"].get_str());
CScript spk = CScript(spk_bytes.begin(), spk_bytes.end());
const auto& script_sig_bytes = ParseHex(input["scriptSig"].get_str());
CScript script_sig = CScript(script_sig_bytes.begin(), script_sig_bytes.end());
CTxIn txin{outpoint, script_sig};
CScriptWitness witness;
// read the field txWitness as a stream and write txWitness >> witness.stack;
auto witness_str = ParseHex(input["txinwitness"].get_str());
if (!witness_str.empty()) {
SpanReader(witness_str) >> witness.stack;
txin.scriptWitness = witness;
}
vin.push_back(txin);
coins[outpoint] = Coin{CTxOut{{}, spk}, 0, false};
}

std::vector<XOnlyPubKey> output_pub_keys;
for (const auto& pubkey : given["outputs"].getValues()) {
output_pub_keys.emplace_back(ParseHex(pubkey.get_str()));
}

CKey scan_priv_key = ParseHexToCKey(given["key_material"]["scan_priv_key"].get_str());
CKey spend_priv_key = ParseHexToCKey(given["key_material"]["spend_priv_key"].get_str());
V0SilentPaymentDestination sp_address{scan_priv_key.GetPubKey(), spend_priv_key.GetPubKey()};

std::map<CPubKey, uint256> labels;
// Always calculate the change key, whether we use labels or not
const auto& [change_pubkey, change_tweak] = BIP352::CreateLabelTweak(scan_priv_key, 0);
labels[change_pubkey] = change_tweak;
// If labels are used, add them to the dictionary
for (const auto& label : given["labels"].getValues()) {
const auto& [label_pubkey, label_tweak] = BIP352::CreateLabelTweak(scan_priv_key, label.getInt<int>());
labels[label_pubkey] = label_tweak;
}


// Scanning
const auto& pub_tweak_data = BIP352::GetSilentPaymentTweakDataFromTxInputs(vin, coins);
// If we don't get any tweak data from the transaction inputs, it is not a silent payment
// transaction, so we skip it.
if (!pub_tweak_data.has_value()) continue;
const auto& found_outputs = BIP352::GetTxOutputTweaks(scan_priv_key, *pub_tweak_data, sp_address.m_spend_pubkey, output_pub_keys, labels);
// The transaction may be a silent payment transaction, but it does not contain any outputs for us,
// so we continue to the next transaction.
if (!found_outputs.has_value()) continue;
std::vector<XOnlyPubKey> expected_outputs;
for (const auto& output : expected["outputs"].getValues()) {
std::string pubkey_hex = output["pub_key"].get_str();
expected_outputs.emplace_back(ParseHex(pubkey_hex));
}
BOOST_CHECK(found_outputs->size() == expected["n_outputs"].getInt<size_t>());
for (const auto& output : *found_outputs) {
BOOST_CHECK(std::find(expected_outputs.begin(), expected_outputs.end(), output.output) != expected_outputs.end());
}
}
}
}
BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet

0 comments on commit 6f238b7

Please sign in to comment.