diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h index 847e5d53a..3452cd4b4 100644 --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -31,6 +31,8 @@ static const unsigned int MAX_TX_SIZE_AFTER_SAPLING = MAX_BLOCK_SIZE; static const int COINBASE_MATURITY = 100; /** The minimum value which is invalid for expiry height, used by CTransaction and CMutableTransaction */ static constexpr uint32_t TX_EXPIRY_HEIGHT_THRESHOLD = 500000000; +/** The number of blocks after Canopy activation after which v1 plaintexts will be rejected */ +static const unsigned int ZIP212_GRACE_PERIOD = 32256; /** Flags for LockTime() */ enum { diff --git a/src/gtest/test_noteencryption.cpp b/src/gtest/test_noteencryption.cpp index 083bdeba1..2d99158b2 100644 --- a/src/gtest/test_noteencryption.cpp +++ b/src/gtest/test_noteencryption.cpp @@ -10,6 +10,8 @@ #include "zcash/Address.hpp" #include "crypto/sha256.h" #include "librustzcash.h" +#include "consensus/params.h" +#include "utiltest.h" class TestNoteDecryption : public ZCNoteDecryption { public: @@ -22,6 +24,13 @@ public: TEST(noteencryption, NotePlaintext) { + SelectParams(CBaseChainParams::REGTEST); + const Consensus::Params& params = Params().GetConsensus(); + int overwinterActivationHeight = 5; + int saplingActivationHeight = 30; + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, overwinterActivationHeight); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, saplingActivationHeight); + using namespace libzcash; auto xsk = SaplingSpendingKey(uint256()).expanded_spending_key(); auto fvk = xsk.full_viewing_key(); @@ -55,6 +64,8 @@ TEST(noteencryption, NotePlaintext) // Try to decrypt with incorrect commitment ASSERT_FALSE(SaplingNotePlaintext::decrypt( + params, + saplingActivationHeight, ct, ivk, epk, @@ -63,6 +74,8 @@ TEST(noteencryption, NotePlaintext) // Try to decrypt with correct commitment auto foo = SaplingNotePlaintext::decrypt( + params, + saplingActivationHeight, ct, ivk, epk, @@ -129,6 +142,8 @@ TEST(noteencryption, NotePlaintext) // Test sender won't accept invalid commitments ASSERT_FALSE( SaplingNotePlaintext::decrypt( + params, + saplingActivationHeight, ct, epk, decrypted_out_ct_unwrapped.esk, @@ -139,6 +154,8 @@ TEST(noteencryption, NotePlaintext) // Test sender can decrypt the note ciphertext. foo = SaplingNotePlaintext::decrypt( + params, + saplingActivationHeight, ct, epk, decrypted_out_ct_unwrapped.esk, diff --git a/src/main.cpp b/src/main.cpp index cbdd8a18d..8e2ebaed9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -921,6 +921,8 @@ bool ContextualCheckTransaction( // SaplingNotePlaintext::decrypt() checks note commitment validity. auto encPlaintext = SaplingNotePlaintext::decrypt( + chainparams.GetConsensus(), + nHeight, output.encCiphertext, output.ephemeralKey, outPlaintext->esk, diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index 52286d00c..1d6eddbb5 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -9,6 +9,7 @@ #include "rpc/protocol.h" #include "script/sign.h" #include "utilmoneystr.h" +#include "zcash/Note.hpp" #include #include @@ -142,6 +143,11 @@ void TransactionBuilder::AddSaplingSpend( throw std::runtime_error("TransactionBuilder cannot add Sapling spend to pre-Sapling transaction"); } + // ZIP212: check that note plaintext lead byte is valid at height + if (!libzcash::plaintext_version_is_valid(consensusParams, nHeight + 1, note.get_lead_byte())) { + throw std::runtime_error("TransactionBuilder: invalid note plaintext version"); + } + // Consistency check: all anchors must equal the first one if (spends.size() > 0 && spends[0].anchor != anchor) { throw JSONRPCError(RPC_WALLET_ERROR, "Anchor does not match previously-added Sapling spends."); diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index 18865cbcd..348b2e022 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -698,6 +698,8 @@ TEST(WalletTests, GetConflictedSaplingNotes) { // Decrypt output note B auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( + consensusParams, + wtx.nExpiryHeight, wtx.vShieldedOutput[0].encCiphertext, ivk, wtx.vShieldedOutput[0].ephemeralKey, @@ -1075,6 +1077,8 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) { // Decrypt note B auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( + consensusParams, + wtx.nExpiryHeight, wtx.vShieldedOutput[0].encCiphertext, ivk, wtx.vShieldedOutput[0].ephemeralKey, @@ -2005,8 +2009,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) { wtx = wallet.mapWallet[hash]; // Prepare to spend the note that was just created - auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( - tx1.vShieldedOutput[0].encCiphertext, ivk, tx1.vShieldedOutput[0].ephemeralKey, tx1.vShieldedOutput[0].cmu); + auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt(consensusParams, fakeIndex.nHeight, tx1.vShieldedOutput[0].encCiphertext, ivk, tx1.vShieldedOutput[0].ephemeralKey, tx1.vShieldedOutput[0].cmu); ASSERT_EQ(static_cast(maybe_pt), true); auto maybe_note = maybe_pt.get().note(ivk); ASSERT_EQ(static_cast(maybe_note), true); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 36838401d..e45aa218e 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5,6 +5,7 @@ #include "amount.h" #include "consensus/upgrades.h" +#include "consensus/params.h" #include "core_io.h" #include "experimental_features.h" #include "init.h" @@ -3768,7 +3769,8 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) auto op = res->second; auto wtxPrev = pwalletMain->mapWallet.at(op.hash); - auto decrypted = wtxPrev.DecryptSaplingNote(op).get(); + // TODO: decide which height to use here instead of wtxPrev.nExpiryHeight + auto decrypted = wtxPrev.DecryptSaplingNote(Params().GetConsensus(), wtxPrev.nExpiryHeight, op).get(); auto notePt = decrypted.first; auto pa = decrypted.second; @@ -3796,14 +3798,16 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) SaplingPaymentAddress pa; bool isOutgoing; - auto decrypted = wtx.DecryptSaplingNote(op); + // TODO: decide which height to use here instead of wtx.nExpiryHeight + auto decrypted = wtx.DecryptSaplingNote(Params().GetConsensus(), wtx.nExpiryHeight, op); if (decrypted) { notePt = decrypted->first; pa = decrypted->second; isOutgoing = false; } else { // Try recovering the output - auto recovered = wtx.RecoverSaplingNote(op, ovks); + // TODO: decide which height to use here instead of wtxPrev.nExpiryHeight + auto recovered = wtx.RecoverSaplingNote(Params().GetConsensus(), wtx.nExpiryHeight, op, ovks); if (recovered) { notePt = recovered->first; pa = recovered->second; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index a1c7bb5c0..0ccea2ae0 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1495,7 +1495,9 @@ void CWallet::UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx) { uint64_t position = nd.witnesses.front().position(); auto extfvk = mapSaplingFullViewingKeys.at(nd.ivk); OutputDescription output = wtx.vShieldedOutput[op.n]; - auto optPlaintext = SaplingNotePlaintext::decrypt(output.encCiphertext, nd.ivk, output.ephemeralKey, output.cmu); + + // TODO: decide which height to use here instead of wtx.nExpiryHeight + auto optPlaintext = SaplingNotePlaintext::decrypt(Params().GetConsensus(), wtx.nExpiryHeight, output.encCiphertext, nd.ivk, output.ephemeralKey, output.cmu); if (!optPlaintext) { // An item in mapSaplingNoteData must have already been successfully decrypted, // otherwise the item would not exist in the first place. @@ -1901,7 +1903,9 @@ std::pair CWallet::FindMySap const OutputDescription output = tx.vShieldedOutput[i]; for (auto it = mapSaplingFullViewingKeys.begin(); it != mapSaplingFullViewingKeys.end(); ++it) { SaplingIncomingViewingKey ivk = it->first; - auto result = SaplingNotePlaintext::decrypt(output.encCiphertext, ivk, output.ephemeralKey, output.cmu); + + // TODO: decide which height to use here instead of wtx.nExpiryHeight + auto result = SaplingNotePlaintext::decrypt(Params().GetConsensus(), tx.nExpiryHeight, output.encCiphertext, ivk, output.ephemeralKey, output.cmu); if (!result) { continue; } @@ -2300,7 +2304,7 @@ std::pair CWalletTx::DecryptSproutNot boost::optional> CWalletTx::DecryptSaplingNote(SaplingOutPoint op) const + SaplingPaymentAddress>> CWalletTx::DecryptSaplingNote(const Consensus::Params& params, int height, SaplingOutPoint op) const { // Check whether we can decrypt this SaplingOutPoint if (this->mapSaplingNoteData.count(op) == 0) { @@ -2311,6 +2315,8 @@ boost::optionalmapSaplingNoteData.at(op); auto maybe_pt = SaplingNotePlaintext::decrypt( + params, + height, output.encCiphertext, nd.ivk, output.ephemeralKey, @@ -2327,8 +2333,7 @@ boost::optional> CWalletTx::RecoverSaplingNote( - SaplingOutPoint op, std::set& ovks) const + SaplingPaymentAddress>> CWalletTx::RecoverSaplingNote(const Consensus::Params& params, int height, SaplingOutPoint op, std::set& ovks) const { auto output = this->vShieldedOutput[op.n]; @@ -2344,6 +2349,8 @@ boost::optionalesk, @@ -4975,7 +4982,10 @@ void CWallet::GetFilteredNotes( SaplingOutPoint op = pair.first; SaplingNoteData nd = pair.second; + // TODO: decide which height to use here instead of wtx.nExpiryHeight auto maybe_pt = SaplingNotePlaintext::decrypt( + Params().GetConsensus(), + wtx.nExpiryHeight, wtx.vShieldedOutput[op.n].encCiphertext, nd.ivk, wtx.vShieldedOutput[op.n].ephemeralKey, diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index c673dfcea..d0696afd6 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -566,10 +566,10 @@ public: JSOutPoint jsop) const; boost::optional> DecryptSaplingNote(SaplingOutPoint op) const; + libzcash::SaplingPaymentAddress>> DecryptSaplingNote(const Consensus::Params& params, int height, SaplingOutPoint op) const; boost::optional> RecoverSaplingNote( + libzcash::SaplingPaymentAddress>> RecoverSaplingNote(const Consensus::Params& params, int height, SaplingOutPoint op, std::set& ovks) const; //! filter decides which addresses will count towards the debit diff --git a/src/zcash/Note.cpp b/src/zcash/Note.cpp index 76a14f9e6..206b73cd9 100644 --- a/src/zcash/Note.cpp +++ b/src/zcash/Note.cpp @@ -1,6 +1,7 @@ #include "Note.hpp" #include "prf.h" #include "crypto/sha256.h" +#include "consensus/consensus.h" #include "random.h" #include "version.h" @@ -188,24 +189,20 @@ boost::optional SaplingOutgoingPlaintext::decrypt( } // Deserialize from the plaintext - try { - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << pt.get(); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << pt.get(); - SaplingOutgoingPlaintext ret; - ss >> ret; + SaplingOutgoingPlaintext ret; + ss >> ret; - assert(ss.size() == 0); + assert(ss.size() == 0); - return ret; - } catch (const boost::thread_interrupted&) { - throw; - } catch (...) { - return boost::none; - } + return ret; } boost::optional SaplingNotePlaintext::decrypt( + const Consensus::Params& params, + int height, const SaplingEncCiphertext &ciphertext, const uint256 &ivk, const uint256 &epk, @@ -219,14 +216,13 @@ boost::optional SaplingNotePlaintext::decrypt( // Deserialize from the plaintext SaplingNotePlaintext ret; - try { - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << pt.get(); - ss >> ret; - assert(ss.size() == 0); - } catch (const boost::thread_interrupted&) { - throw; - } catch (...) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << pt.get(); + ss >> ret; + assert(ss.size() == 0); + + // Check leadbyte is allowed at block height + if (!plaintext_version_valid(params, height, ret.leadByte)) { return boost::none; } @@ -269,6 +265,8 @@ boost::optional SaplingNotePlaintext::decrypt( } boost::optional SaplingNotePlaintext::decrypt( + const Consensus::Params& params, + int height, const SaplingEncCiphertext &ciphertext, const uint256 &epk, const uint256 &esk, @@ -283,14 +281,13 @@ boost::optional SaplingNotePlaintext::decrypt( // Deserialize from the plaintext SaplingNotePlaintext ret; - try { - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << pt.get(); - ss >> ret; - assert(ss.size() == 0); - } catch (const boost::thread_interrupted&) { - throw; - } catch (...) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << pt.get(); + ss >> ret; + assert(ss.size() == 0); + + // Check leadbyte is legible at block height + if (!plaintext_version_valid(params, height, ret.leadByte)) { return boost::none; } diff --git a/src/zcash/Note.hpp b/src/zcash/Note.hpp index a56a2310e..134c289eb 100644 --- a/src/zcash/Note.hpp +++ b/src/zcash/Note.hpp @@ -5,6 +5,8 @@ #include "Zcash.h" #include "Address.hpp" #include "NoteEncryption.hpp" +#include "consensus/params.h" +#include "consensus/consensus.h" #include #include @@ -40,6 +42,27 @@ public: uint256 nullifier(const SproutSpendingKey& a_sk) const; }; +inline bool plaintext_version_is_valid(const Consensus::Params& params, int height, unsigned char leadByte) { + int canopyActivationHeight = params.vUpgrades[Consensus::UPGRADE_CANOPY].nActivationHeight; + + if (height < canopyActivationHeight && leadByte != 0x01) { + // non-0x01 received before Canopy activation height + return false; + } + if (height >= canopyActivationHeight + && height < canopyActivationHeight + ZIP212_GRACE_PERIOD + && leadByte != 0x01 + && leadByte != 0x02) + { + // non-{0x01,0x02} received after Canopy activation and before grace period has elapsed + return false; + } + if (height >= canopyActivationHeight + ZIP212_GRACE_PERIOD && leadByte != 0x02) { + // non-0x02 received past (Canopy activation height + grace period) + return false; + } + return true; +}; class SaplingNote : public BaseNote { private: @@ -60,6 +83,10 @@ public: boost::optional cmu() const; boost::optional nullifier(const SaplingFullViewingKey &vk, const uint64_t position) const; uint256 rcm() const; + + unsigned char get_lead_byte() const { + return leadByte; + } }; class BaseNotePlaintext { @@ -132,6 +159,8 @@ public: SaplingNotePlaintext(const SaplingNote& note, std::array memo); static boost::optional decrypt( + const Consensus::Params& params, + int height, const SaplingEncCiphertext &ciphertext, const uint256 &ivk, const uint256 &epk, @@ -139,6 +168,8 @@ public: ); static boost::optional decrypt( + const Consensus::Params& params, + int height, const SaplingEncCiphertext &ciphertext, const uint256 &epk, const uint256 &esk, @@ -154,13 +185,7 @@ public: template inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(leadByte); - - if (leadByte != 0x01 && leadByte != 0x02) { - printf("leadByte: %x\n", leadByte); - throw std::ios_base::failure("lead byte of SaplingNotePlaintext is not recognized"); - } - + READWRITE(leadByte); // 1 byte READWRITE(d); // 11 bytes READWRITE(value_); // 8 bytes READWRITE(rseed); // 32 bytes @@ -171,7 +196,7 @@ public: uint256 rcm() const; uint256 generate_esk() const; - bool get_lead_byte() const { + unsigned char get_lead_byte() const { return leadByte; } };