Reject v1 plaintexts after grace period
SaplingNotePlaintext::decrypt() now has to be aware of consensus params and blockheight. Its callers in wallet, rpcwallet, and tests are updated accordingly. TransactionBuilder is also modified to reject invalid leadBytes. Co-authored by Daira Hopwood (daira@jacaranda.org)
This commit is contained in:
parent
56d4ef8333
commit
e060d59890
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include "rpc/protocol.h"
|
||||
#include "script/sign.h"
|
||||
#include "utilmoneystr.h"
|
||||
#include "zcash/Note.hpp"
|
||||
|
||||
#include <boost/variant.hpp>
|
||||
#include <librustzcash.h>
|
||||
|
@ -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.");
|
||||
|
|
|
@ -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<bool>(maybe_pt), true);
|
||||
auto maybe_note = maybe_pt.get().note(ivk);
|
||||
ASSERT_EQ(static_cast<bool>(maybe_note), true);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<mapSaplingNoteData_t, SaplingIncomingViewingKeyMap> 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<SproutNotePlaintext, SproutPaymentAddress> CWalletTx::DecryptSproutNot
|
|||
|
||||
boost::optional<std::pair<
|
||||
SaplingNotePlaintext,
|
||||
SaplingPaymentAddress>> 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::optional<std::pair<
|
|||
auto nd = this->mapSaplingNoteData.at(op);
|
||||
|
||||
auto maybe_pt = SaplingNotePlaintext::decrypt(
|
||||
params,
|
||||
height,
|
||||
output.encCiphertext,
|
||||
nd.ivk,
|
||||
output.ephemeralKey,
|
||||
|
@ -2327,8 +2333,7 @@ boost::optional<std::pair<
|
|||
|
||||
boost::optional<std::pair<
|
||||
SaplingNotePlaintext,
|
||||
SaplingPaymentAddress>> CWalletTx::RecoverSaplingNote(
|
||||
SaplingOutPoint op, std::set<uint256>& ovks) const
|
||||
SaplingPaymentAddress>> CWalletTx::RecoverSaplingNote(const Consensus::Params& params, int height, SaplingOutPoint op, std::set<uint256>& ovks) const
|
||||
{
|
||||
auto output = this->vShieldedOutput[op.n];
|
||||
|
||||
|
@ -2344,6 +2349,8 @@ boost::optional<std::pair<
|
|||
}
|
||||
|
||||
auto maybe_pt = SaplingNotePlaintext::decrypt(
|
||||
params,
|
||||
height,
|
||||
output.encCiphertext,
|
||||
output.ephemeralKey,
|
||||
outPt->esk,
|
||||
|
@ -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,
|
||||
|
|
|
@ -566,10 +566,10 @@ public:
|
|||
JSOutPoint jsop) const;
|
||||
boost::optional<std::pair<
|
||||
libzcash::SaplingNotePlaintext,
|
||||
libzcash::SaplingPaymentAddress>> DecryptSaplingNote(SaplingOutPoint op) const;
|
||||
libzcash::SaplingPaymentAddress>> DecryptSaplingNote(const Consensus::Params& params, int height, SaplingOutPoint op) const;
|
||||
boost::optional<std::pair<
|
||||
libzcash::SaplingNotePlaintext,
|
||||
libzcash::SaplingPaymentAddress>> RecoverSaplingNote(
|
||||
libzcash::SaplingPaymentAddress>> RecoverSaplingNote(const Consensus::Params& params, int height,
|
||||
SaplingOutPoint op, std::set<uint256>& ovks) const;
|
||||
|
||||
//! filter decides which addresses will count towards the debit
|
||||
|
|
|
@ -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> 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> SaplingNotePlaintext::decrypt(
|
||||
const Consensus::Params& params,
|
||||
int height,
|
||||
const SaplingEncCiphertext &ciphertext,
|
||||
const uint256 &ivk,
|
||||
const uint256 &epk,
|
||||
|
@ -219,14 +216,13 @@ boost::optional<SaplingNotePlaintext> 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> SaplingNotePlaintext::decrypt(
|
|||
}
|
||||
|
||||
boost::optional<SaplingNotePlaintext> SaplingNotePlaintext::decrypt(
|
||||
const Consensus::Params& params,
|
||||
int height,
|
||||
const SaplingEncCiphertext &ciphertext,
|
||||
const uint256 &epk,
|
||||
const uint256 &esk,
|
||||
|
@ -283,14 +281,13 @@ boost::optional<SaplingNotePlaintext> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#include "Zcash.h"
|
||||
#include "Address.hpp"
|
||||
#include "NoteEncryption.hpp"
|
||||
#include "consensus/params.h"
|
||||
#include "consensus/consensus.h"
|
||||
|
||||
#include <array>
|
||||
#include <boost/optional.hpp>
|
||||
|
@ -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<uint256> cmu() const;
|
||||
boost::optional<uint256> 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<unsigned char, ZC_MEMO_SIZE> memo);
|
||||
|
||||
static boost::optional<SaplingNotePlaintext> decrypt(
|
||||
const Consensus::Params& params,
|
||||
int height,
|
||||
const SaplingEncCiphertext &ciphertext,
|
||||
const uint256 &ivk,
|
||||
const uint256 &epk,
|
||||
|
@ -139,6 +168,8 @@ public:
|
|||
);
|
||||
|
||||
static boost::optional<SaplingNotePlaintext> decrypt(
|
||||
const Consensus::Params& params,
|
||||
int height,
|
||||
const SaplingEncCiphertext &ciphertext,
|
||||
const uint256 &epk,
|
||||
const uint256 &esk,
|
||||
|
@ -154,13 +185,7 @@ public:
|
|||
|
||||
template <typename Stream, typename Operation>
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue