From 02e674555ef2f9ecf669503f106414badda77ae7 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 24 Aug 2016 15:50:45 +1200 Subject: [PATCH] Add wallet method for finding spendable notes in a CTransaction --- src/Makefile.gtest.include | 3 +- src/keystore.cpp | 4 +- src/keystore.h | 29 ++++++++++ src/wallet/gtest/test_wallet.cpp | 98 ++++++++++++++++++++++++++++++++ src/wallet/wallet.cpp | 43 ++++++++++++++ src/wallet/wallet.h | 84 +++++++++++++++++++++++++++ src/zcash/Address.hpp | 1 + src/zcash/NoteEncryption.hpp | 4 ++ 8 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 src/wallet/gtest/test_wallet.cpp diff --git a/src/Makefile.gtest.include b/src/Makefile.gtest.include index 8e22ed397..e69deb89f 100644 --- a/src/Makefile.gtest.include +++ b/src/Makefile.gtest.include @@ -16,7 +16,8 @@ zcash_gtest_SOURCES = \ gtest/test_txid.cpp \ gtest/test_wallet_zkeys.cpp \ gtest/test_libzcash_utils.cpp \ - gtest/test_proofs.cpp + gtest/test_proofs.cpp \ + wallet/gtest/test_wallet.cpp zcash_gtest_CPPFLAGS = -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC diff --git a/src/keystore.cpp b/src/keystore.cpp index 7240cd747..251e47e26 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -87,6 +87,8 @@ bool CBasicKeyStore::HaveWatchOnly() const bool CBasicKeyStore::AddSpendingKey(const libzcash::SpendingKey &sk) { LOCK(cs_SpendingKeyStore); - mapSpendingKeys[sk.address()] = sk; + auto address = sk.address(); + mapSpendingKeys[address] = sk; + mapNoteDecryptors[address] = ZCNoteDecryption(sk.viewing_key()); return true; } diff --git a/src/keystore.h b/src/keystore.h index 987f32070..478c06cb8 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -12,6 +12,7 @@ #include "script/standard.h" #include "sync.h" #include "zcash/Address.hpp" +#include "zcash/NoteEncryption.hpp" #include #include @@ -60,6 +61,7 @@ typedef std::map KeyMap; typedef std::map ScriptMap; typedef std::set WatchOnlySet; typedef std::map SpendingKeyMap; +typedef std::map NoteDecryptorMap; /** Basic key store, that keeps keys in an address->secret map */ class CBasicKeyStore : public CKeyStore @@ -69,6 +71,7 @@ protected: ScriptMap mapScripts; WatchOnlySet setWatchOnly; SpendingKeyMap mapSpendingKeys; + NoteDecryptorMap mapNoteDecryptors; public: bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey); @@ -139,6 +142,32 @@ public: } return false; } + bool GetNoteDecryptor(const libzcash::PaymentAddress &address, ZCNoteDecryption &decOut) const + { + { + LOCK(cs_KeyStore); + NoteDecryptorMap::const_iterator mi = mapNoteDecryptors.find(address); + if (mi != mapNoteDecryptors.end()) + { + decOut = mi->second; + return true; + } + } + return false; + } + void GetNoteDecryptors(std::set &setDec) const + { + setDec.clear(); + { + LOCK(cs_SpendingKeyStore); + NoteDecryptorMap::const_iterator mi = mapNoteDecryptors.begin(); + while (mi != mapNoteDecryptors.end()) + { + setDec.insert(*mi); + mi++; + } + } + } void GetPaymentAddresses(std::set &setAddress) const { setAddress.clear(); diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp new file mode 100644 index 000000000..bbb0501bb --- /dev/null +++ b/src/wallet/gtest/test_wallet.cpp @@ -0,0 +1,98 @@ +#include +#include + +#include "base58.h" +#include "chainparams.h" +#include "random.h" +#include "wallet/wallet.h" +#include "zcash/JoinSplit.hpp" +#include "zcash/Note.hpp" +#include "zcash/NoteEncryption.hpp" + +ZCJoinSplit* params = ZCJoinSplit::Unopened(); + +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(), // dummy output + 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()); +} + +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(1, noteMap.size()); + + JSOutPoint jsoutpt {wtx.GetTxid(), 0, 1}; + CNoteData nd {sk.address(), nullifier}; + EXPECT_EQ(1, noteMap.count(jsoutpt)); + EXPECT_EQ(nd, noteMap[jsoutpt]); +} diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 0f6eeff49..cb977ec8e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -10,6 +10,7 @@ #include "coincontrol.h" #include "consensus/consensus.h" #include "consensus/validation.h" +#include "init.h" #include "main.h" #include "net.h" #include "script/script.h" @@ -17,6 +18,7 @@ #include "timedata.h" #include "util.h" #include "utilmoneystr.h" +#include "zcash/Note.hpp" #include @@ -57,6 +59,11 @@ struct CompareValueOnly } }; +std::string JSOutPoint::ToString() const +{ + return strprintf("JSOutPoint(%s, %d, %d)", hash.ToString().substr(0,10), js, n); +} + std::string COutput::ToString() const { return strprintf("COutput(%s, %d, %d) [%s]", tx->GetTxid().ToString(), i, nDepth, FormatMoney(tx->vout[i].nValue)); @@ -843,6 +850,42 @@ void CWallet::EraseFromWallet(const uint256 &hash) } +mapNoteData_t CWallet::FindMyNotes(const CTransaction& tx) const +{ + uint256 hash = tx.GetTxid(); + + mapNoteData_t noteData; + std::set decryptors; + GetNoteDecryptors(decryptors); + libzcash::SpendingKey key; + for (size_t i = 0; i < tx.vjoinsplit.size(); i++) { + auto hSig = tx.vjoinsplit[i].h_sig(*pzcashParams, tx.joinSplitPubKey); + for (uint8_t j = 0; j < tx.vjoinsplit[i].ciphertexts.size(); j++) { + for (const NoteDecryptorMap::value_type& item : decryptors) { + try { + auto note_pt = libzcash::NotePlaintext::decrypt( + item.second, + tx.vjoinsplit[i].ciphertexts[j], + tx.vjoinsplit[i].ephemeralKey, + hSig, + (unsigned char) j); + auto address = item.first; + // Decryptors are only cached when SpendingKeys are added + assert(GetSpendingKey(address, key)); + auto note = note_pt.note(address); + JSOutPoint jsoutpt {hash, i, j}; + CNoteData nd {address, note.nullifier(key)}; + noteData.insert(std::make_pair(jsoutpt, nd)); + break; + } catch (const std::exception &) { + // Couldn't decrypt with this spending key + } + } + } + } + return noteData; +} + isminetype CWallet::IsMine(const CTxIn &txin) const { { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 0651c6404..4bbbe2aa0 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -146,6 +146,88 @@ struct COutputEntry int vout; }; +/** An note outpoint */ +class JSOutPoint +{ +public: + // Transaction hash + uint256 hash; + // Index into CTransaction.vjoinsplit + size_t js; + // Index into JSDescription fields of length ZC_NUM_JS_OUTPUTS + uint8_t n; + + JSOutPoint() { SetNull(); } + JSOutPoint(uint256 h, size_t js, uint8_t n) : hash {h}, js {js}, n {n} { } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { + READWRITE(hash); + READWRITE(js); + READWRITE(n); + } + + void SetNull() { hash.SetNull(); } + bool IsNull() const { return hash.IsNull(); } + + friend bool operator<(const JSOutPoint& a, const JSOutPoint& b) { + return (a.hash < b.hash || + (a.hash == b.hash && a.js < b.js) || + (a.hash == b.hash && a.js == b.js && a.n < b.n)); + } + + friend bool operator==(const JSOutPoint& a, const JSOutPoint& b) { + return (a.hash == b.hash && a.js == b.js && a.n == b.n); + } + + friend bool operator!=(const JSOutPoint& a, const JSOutPoint& b) { + return !(a == b); + } + + std::string ToString() const; +}; + +class CNoteData +{ +public: + libzcash::PaymentAddress address; + + // It's okay to cache the nullifier in the wallet, because we are storing + // the spending key there too, which could be used to derive this. + // If PR #1210 is merged, we need to revisit the threat model and decide + // whether it is okay to store this unencrypted while the spending key is + // encrypted. + uint256 nullifier; + + CNoteData() : address(), nullifier() { } + CNoteData(libzcash::PaymentAddress a, uint256 n) : address {a}, nullifier {n} { } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { + READWRITE(address); + READWRITE(nullifier); + } + + friend bool operator<(const CNoteData& a, const CNoteData& b) { + return (a.address < b.address || + (a.address == b.address && a.nullifier < b.nullifier)); + } + + friend bool operator==(const CNoteData& a, const CNoteData& b) { + return (a.address == b.address && a.nullifier == b.nullifier); + } + + friend bool operator!=(const CNoteData& a, const CNoteData& b) { + return !(a == b); + } +}; + +typedef std::map mapNoteData_t; + /** A transaction with a merkle branch linking it to the block chain. */ class CMerkleTx : public CTransaction { @@ -667,6 +749,8 @@ public: std::set GetAccountAddresses(std::string strAccount) const; + mapNoteData_t FindMyNotes(const CTransaction& tx) const; + isminetype IsMine(const CTxIn& txin) const; CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const; isminetype IsMine(const CTxOut& txout) const; diff --git a/src/zcash/Address.hpp b/src/zcash/Address.hpp index 36b9402a3..d967aef27 100644 --- a/src/zcash/Address.hpp +++ b/src/zcash/Address.hpp @@ -23,6 +23,7 @@ public: READWRITE(pk_enc); } + friend inline bool operator==(const PaymentAddress& a, const PaymentAddress& b) { return a.a_pk == b.a_pk && a.pk_enc == b.pk_enc; } friend inline bool operator<(const PaymentAddress& a, const PaymentAddress& b) { return a.a_pk < b.a_pk; } }; diff --git a/src/zcash/NoteEncryption.hpp b/src/zcash/NoteEncryption.hpp index 7161d5a20..1cd1a9b27 100644 --- a/src/zcash/NoteEncryption.hpp +++ b/src/zcash/NoteEncryption.hpp @@ -61,6 +61,8 @@ public: typedef boost::array Ciphertext; typedef boost::array Plaintext; + // Unused default constructor to make allocators happy + NoteDecryption() { } NoteDecryption(uint256 sk_enc); Plaintext decrypt(const Ciphertext &ciphertext, @@ -68,6 +70,8 @@ public: const uint256 &hSig, unsigned char nonce ) const; + + friend inline bool operator<(const NoteDecryption& a, const NoteDecryption& b) { return a.pk_enc < b.pk_enc; } }; uint256 random_uint256();