#include #include #include #include "base58.h" #include "chainparams.h" #include "main.h" #include "random.h" #include "wallet/wallet.h" #include "zcash/JoinSplit.hpp" #include "zcash/Note.hpp" #include "zcash/NoteEncryption.hpp" using ::testing::_; using ::testing::Return; ZCJoinSplit* params = ZCJoinSplit::Unopened(); class TestWallet : public CWallet { public: TestWallet() : CWallet() { } void IncrementNoteWitnesses(const CBlockIndex* pindex, const CBlock* pblock, ZCIncrementalMerkleTree tree) { CWallet::IncrementNoteWitnesses(pindex, pblock, tree); } void DecrementNoteWitnesses() { CWallet::DecrementNoteWitnesses(); } }; CWalletTx GetValidReceive(const libzcash::SpendingKey& sk, CAmount value, bool randomInputs) { CMutableTransaction mtx; mtx.nVersion = 2; // Enable JoinSplits mtx.vin.resize(2); if (randomInputs) { mtx.vin[0].prevout.hash = GetRandHash(); mtx.vin[1].prevout.hash = GetRandHash(); } else { mtx.vin[0].prevout.hash = uint256S("0000000000000000000000000000000000000000000000000000000000000001"); mtx.vin[1].prevout.hash = uint256S("0000000000000000000000000000000000000000000000000000000000000002"); } mtx.vin[0].prevout.n = 0; mtx.vin[1].prevout.n = 0; // Generate an ephemeral keypair. uint256 joinSplitPubKey; unsigned char joinSplitPrivKey[crypto_sign_SECRETKEYBYTES]; crypto_sign_keypair(joinSplitPubKey.begin(), joinSplitPrivKey); mtx.joinSplitPubKey = joinSplitPubKey; boost::array inputs = { libzcash::JSInput(), // dummy input libzcash::JSInput() // dummy input }; boost::array outputs = { libzcash::JSOutput(sk.address(), value), libzcash::JSOutput(sk.address(), value) }; boost::array output_notes; // Prepare JoinSplits uint256 rt; JSDescription jsdesc {*params, mtx.joinSplitPubKey, rt, inputs, outputs, value, 0, false}; mtx.vjoinsplit.push_back(jsdesc); // Empty output script. CScript scriptCode; CTransaction signTx(mtx); uint256 dataToBeSigned = SignatureHash(scriptCode, signTx, NOT_AN_INPUT, SIGHASH_ALL); // Add the signature assert(crypto_sign_detached(&mtx.joinSplitSig[0], NULL, dataToBeSigned.begin(), 32, joinSplitPrivKey ) == 0); CTransaction tx {mtx}; CWalletTx wtx {NULL, tx}; return wtx; } libzcash::Note GetNote(const libzcash::SpendingKey& sk, const CTransaction& tx, size_t js, size_t n) { ZCNoteDecryption decryptor {sk.viewing_key()}; auto hSig = tx.vjoinsplit[js].h_sig(*params, tx.joinSplitPubKey); auto note_pt = libzcash::NotePlaintext::decrypt( decryptor, tx.vjoinsplit[js].ciphertexts[n], tx.vjoinsplit[js].ephemeralKey, hSig, (unsigned char) n); return note_pt.note(sk.address()); } CWalletTx GetValidSpend(const libzcash::SpendingKey& sk, const libzcash::Note& note, CAmount value) { CMutableTransaction mtx; mtx.vout.resize(2); mtx.vout[0].nValue = value; mtx.vout[1].nValue = 0; // Generate an ephemeral keypair. uint256 joinSplitPubKey; unsigned char joinSplitPrivKey[crypto_sign_SECRETKEYBYTES]; crypto_sign_keypair(joinSplitPubKey.begin(), joinSplitPrivKey); mtx.joinSplitPubKey = joinSplitPubKey; // Fake tree for the unused witness ZCIncrementalMerkleTree tree; boost::array inputs = { libzcash::JSInput(tree.witness(), note, sk), libzcash::JSInput() // dummy input }; boost::array outputs = { libzcash::JSOutput(), // dummy output libzcash::JSOutput() // dummy output }; boost::array output_notes; // Prepare JoinSplits uint256 rt; JSDescription jsdesc {*params, mtx.joinSplitPubKey, rt, inputs, outputs, 0, value, false}; mtx.vjoinsplit.push_back(jsdesc); // Empty output script. CScript scriptCode; CTransaction signTx(mtx); uint256 dataToBeSigned = SignatureHash(scriptCode, signTx, NOT_AN_INPUT, SIGHASH_ALL); // Add the signature assert(crypto_sign_detached(&mtx.joinSplitSig[0], NULL, dataToBeSigned.begin(), 32, joinSplitPrivKey ) == 0); CTransaction tx {mtx}; CWalletTx wtx {NULL, tx}; return wtx; } TEST(wallet_tests, note_data_serialisation) { auto sk = libzcash::SpendingKey::random(); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; ZCIncrementalMerkleTree tree; nd.witnesses.push_front(tree.witness()); noteData[jsoutpt] = nd; CDataStream ss(SER_DISK, CLIENT_VERSION); ss << noteData; mapNoteData_t noteData2; ss >> noteData2; EXPECT_EQ(noteData, noteData2); EXPECT_EQ(noteData[jsoutpt].witnesses, noteData2[jsoutpt].witnesses); } TEST(wallet_tests, set_note_addrs_in_cwallettx) { auto sk = libzcash::SpendingKey::random(); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); EXPECT_EQ(0, wtx.mapNoteData.size()); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetNoteData(noteData); EXPECT_EQ(noteData, wtx.mapNoteData); } TEST(wallet_tests, set_invalid_note_addrs_in_cwallettx) { CWalletTx wtx; EXPECT_EQ(0, wtx.mapNoteData.size()); mapNoteData_t noteData; auto sk = libzcash::SpendingKey::random(); JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), uint256()}; noteData[jsoutpt] = nd; EXPECT_THROW(wtx.SetNoteData(noteData), std::logic_error); } TEST(wallet_tests, find_note_in_tx) { CWallet wallet; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); auto noteMap = wallet.FindMyNotes(wtx); EXPECT_EQ(2, noteMap.size()); JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; EXPECT_EQ(1, noteMap.count(jsoutpt)); EXPECT_EQ(nd, noteMap[jsoutpt]); } TEST(wallet_tests, get_conflicted_notes) { CWallet wallet; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); auto wtx2 = GetValidSpend(sk, note, 5); auto wtx3 = GetValidSpend(sk, note, 10); auto hash2 = wtx2.GetHash(); auto hash3 = wtx3.GetHash(); // No conflicts for no spends EXPECT_EQ(0, wallet.GetConflicts(hash2).size()); wallet.AddToWallet(wtx, true, NULL); EXPECT_EQ(0, wallet.GetConflicts(hash2).size()); // No conflicts for one spend wallet.AddToWallet(wtx2, true, NULL); EXPECT_EQ(0, wallet.GetConflicts(hash2).size()); // Conflicts for two spends wallet.AddToWallet(wtx3, true, NULL); auto c3 = wallet.GetConflicts(hash2); EXPECT_EQ(2, c3.size()); EXPECT_EQ(std::set({hash2, hash3}), c3); } TEST(wallet_tests, nullifier_is_spent) { CWallet wallet; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); EXPECT_FALSE(wallet.IsSpent(nullifier)); wallet.AddToWallet(wtx, true, NULL); EXPECT_FALSE(wallet.IsSpent(nullifier)); auto wtx2 = GetValidSpend(sk, note, 5); wallet.AddToWallet(wtx2, true, NULL); EXPECT_FALSE(wallet.IsSpent(nullifier)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); CBlock block; block.vtx.push_back(wtx2); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); wtx2.SetMerkleBranch(block); wallet.AddToWallet(wtx2, true, NULL); EXPECT_TRUE(wallet.IsSpent(nullifier)); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); } TEST(wallet_tests, navigate_from_nullifier_to_note) { CWallet wallet; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetNoteData(noteData); EXPECT_EQ(0, wallet.mapNullifiersToNotes.count(nullifier)); wallet.AddToWallet(wtx, true, NULL); EXPECT_EQ(1, wallet.mapNullifiersToNotes.count(nullifier)); EXPECT_EQ(wtx.GetHash(), wallet.mapNullifiersToNotes[nullifier].hash); EXPECT_EQ(0, wallet.mapNullifiersToNotes[nullifier].js); EXPECT_EQ(1, wallet.mapNullifiersToNotes[nullifier].n); } TEST(wallet_tests, spent_note_is_from_me) { CWallet wallet; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); auto wtx2 = GetValidSpend(sk, note, 5); EXPECT_FALSE(wallet.IsFromMe(wtx)); EXPECT_FALSE(wallet.IsFromMe(wtx2)); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetNoteData(noteData); EXPECT_FALSE(wallet.IsFromMe(wtx)); EXPECT_FALSE(wallet.IsFromMe(wtx2)); wallet.AddToWallet(wtx, true, NULL); EXPECT_FALSE(wallet.IsFromMe(wtx)); EXPECT_TRUE(wallet.IsFromMe(wtx2)); } TEST(wallet_tests, cached_witnesses_empty_chain) { TestWallet wallet; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 0); auto note2 = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); auto nullifier2 = note2.nullifier(sk); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 0}; JSOutPoint jsoutpt2 {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; CNoteData nd2 {sk.address(), nullifier2}; noteData[jsoutpt] = nd; noteData[jsoutpt2] = nd2; wtx.SetNoteData(noteData); std::vector notes {jsoutpt, jsoutpt2}; std::vector> witnesses; uint256 anchor; wallet.GetNoteWitnesses(notes, witnesses, anchor); EXPECT_FALSE((bool) witnesses[0]); EXPECT_FALSE((bool) witnesses[1]); wallet.AddToWallet(wtx, true, NULL); witnesses.clear(); wallet.GetNoteWitnesses(notes, witnesses, anchor); EXPECT_FALSE((bool) witnesses[0]); EXPECT_FALSE((bool) witnesses[1]); CBlock block; block.vtx.push_back(wtx); ZCIncrementalMerkleTree tree; wallet.IncrementNoteWitnesses(NULL, &block, tree); witnesses.clear(); wallet.GetNoteWitnesses(notes, witnesses, anchor); EXPECT_TRUE((bool) witnesses[0]); EXPECT_TRUE((bool) witnesses[1]); // Until #1302 is implemented, this should triggger an assertion EXPECT_DEATH(wallet.DecrementNoteWitnesses(), "Assertion `nWitnessCacheSize > 0' failed."); } TEST(wallet_tests, cached_witnesses_chain_tip) { TestWallet wallet; uint256 anchor1; CBlock block1; ZCIncrementalMerkleTree tree; auto sk = libzcash::SpendingKey::random(); wallet.AddSpendingKey(sk); { // First transaction (case tested in _empty_chain) auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetNoteData(noteData); wallet.AddToWallet(wtx, true, NULL); std::vector notes {jsoutpt}; std::vector> witnesses; // First block (case tested in _empty_chain) block1.vtx.push_back(wtx); wallet.IncrementNoteWitnesses(NULL, &block1, tree); // Called to fetch anchor wallet.GetNoteWitnesses(notes, witnesses, anchor1); } { // Second transaction auto wtx = GetValidReceive(sk, 50, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; CNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetNoteData(noteData); wallet.AddToWallet(wtx, true, NULL); std::vector notes {jsoutpt}; std::vector> witnesses; uint256 anchor2; wallet.GetNoteWitnesses(notes, witnesses, anchor2); EXPECT_FALSE((bool) witnesses[0]); // Second block CBlock block2; block2.hashPrevBlock = block1.GetHash(); block2.vtx.push_back(wtx); ZCIncrementalMerkleTree tree2 {tree}; wallet.IncrementNoteWitnesses(NULL, &block2, tree2); witnesses.clear(); wallet.GetNoteWitnesses(notes, witnesses, anchor2); EXPECT_TRUE((bool) witnesses[0]); EXPECT_NE(anchor1, anchor2); // Decrementing should give us the previous anchor uint256 anchor3; wallet.DecrementNoteWitnesses(); witnesses.clear(); wallet.GetNoteWitnesses(notes, witnesses, anchor3); EXPECT_FALSE((bool) witnesses[0]); // Should not equal first anchor because none of these notes had witnesses EXPECT_NE(anchor1, anchor3); // Re-incrementing with the same block should give the same result uint256 anchor4; wallet.IncrementNoteWitnesses(NULL, &block2, tree); witnesses.clear(); wallet.GetNoteWitnesses(notes, witnesses, anchor4); EXPECT_TRUE((bool) witnesses[0]); EXPECT_EQ(anchor2, anchor4); } }