Skip to content

Commit ead9b1d

Browse files
committed
tests: add BIP352 test vectors as unit tests
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
1 parent f96459c commit ead9b1d

File tree

3 files changed

+2950
-0
lines changed

3 files changed

+2950
-0
lines changed

src/test/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ add_executable(test_bitcoin
1818
bech32_tests.cpp
1919
bip32_tests.cpp
2020
bip324_tests.cpp
21+
bip352_tests.cpp
2122
blockchain_tests.cpp
2223
blockencodings_tests.cpp
2324
blockfilter_index_tests.cpp
@@ -130,6 +131,7 @@ include(TargetDataSources)
130131
target_json_data_sources(test_bitcoin
131132
data/base58_encode_decode.json
132133
data/bip341_wallet_vectors.json
134+
data/bip352_send_and_receive_vectors.json
133135
data/blockfilters.json
134136
data/key_io_invalid.json
135137
data/key_io_valid.json

src/test/bip352_tests.cpp

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)