diff --git a/src/test/rpc_wallet_tests.cpp b/src/test/rpc_wallet_tests.cpp index 3bc729f6f..6b132d0a1 100644 --- a/src/test/rpc_wallet_tests.cpp +++ b/src/test/rpc_wallet_tests.cpp @@ -1254,6 +1254,108 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) } +BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling) +{ + SelectParams(CBaseChainParams::REGTEST); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + + LOCK(pwalletMain->cs_wallet); + + if (!pwalletMain->HaveHDSeed()) { + pwalletMain->GenerateNewSeed(); + } + + UniValue retValue; + + // add keys manually + auto taddr = pwalletMain->GenerateNewKey().GetID(); + std::string taddr1 = EncodeDestination(taddr); + auto pa = pwalletMain->GenerateNewSaplingZKey(); + std::string zaddr1 = EncodePaymentAddress(pa); + + auto consensusParams = Params().GetConsensus(); + retValue = CallRPC("getblockcount"); + int nextBlockHeight = retValue.get_int() + 1; + + // Add a fake transaction to the wallet + CMutableTransaction mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight); + CScript scriptPubKey = CScript() << OP_DUP << OP_HASH160 << ToByteVector(taddr) << OP_EQUALVERIFY << OP_CHECKSIG; + mtx.vout.push_back(CTxOut(5 * COIN, scriptPubKey)); + CWalletTx wtx(pwalletMain, mtx); + pwalletMain->AddToWallet(wtx, true, NULL); + + // Fake-mine the transaction + BOOST_CHECK_EQUAL(0, chainActive.Height()); + CBlock block; + block.hashPrevBlock = chainActive.Tip()->GetBlockHash(); + block.vtx.push_back(wtx); + block.hashMerkleRoot = block.BuildMerkleTree(); + auto blockHash = block.GetHash(); + CBlockIndex fakeIndex {block}; + fakeIndex.nHeight = 1; + mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); + chainActive.SetTip(&fakeIndex); + BOOST_CHECK(chainActive.Contains(&fakeIndex)); + BOOST_CHECK_EQUAL(1, chainActive.Height()); + wtx.SetMerkleBranch(block); + pwalletMain->AddToWallet(wtx, true, NULL); + + // Context that z_sendmany requires + auto builder = TransactionBuilder(consensusParams, nextBlockHeight, pwalletMain); + mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight); + + std::vector recipients = { SendManyRecipient(zaddr1, 1 * COIN, "ABCD") }; + std::shared_ptr operation( new AsyncRPCOperation_sendmany(builder, mtx, taddr1, {}, recipients, 0) ); + std::shared_ptr ptr = std::dynamic_pointer_cast (operation); + + // Enable test mode so tx is not sent + static_cast(operation.get())->testmode = true; + + // Generate the Sapling shielding transaction + operation->main(); + BOOST_CHECK(operation->isSuccess()); + + // Get the transaction + auto result = operation->getResult(); + BOOST_ASSERT(result.isObject()); + auto hexTx = result["hex"].getValStr(); + CDataStream ss(ParseHex(hexTx), SER_NETWORK, PROTOCOL_VERSION); + CTransaction tx; + ss >> tx; + BOOST_ASSERT(!tx.vShieldedOutput.empty()); + + // We shouldn't be able to decrypt with the empty ovk + BOOST_CHECK(!AttemptSaplingOutDecryption( + tx.vShieldedOutput[0].outCiphertext, + uint256(), + tx.vShieldedOutput[0].cv, + tx.vShieldedOutput[0].cm, + tx.vShieldedOutput[0].ephemeralKey)); + + // We should be able to decrypt the outCiphertext with the ovk + // generated for transparent addresses + HDSeed seed; + BOOST_ASSERT(pwalletMain->GetHDSeed(seed)); + BOOST_CHECK(AttemptSaplingOutDecryption( + tx.vShieldedOutput[0].outCiphertext, + ovkForShieldingFromTaddr(seed), + tx.vShieldedOutput[0].cv, + tx.vShieldedOutput[0].cm, + tx.vShieldedOutput[0].ephemeralKey)); + + // Tear down + chainActive.SetTip(NULL); + mapBlockIndex.erase(blockHash); + mapArgs.erase("-developersapling"); + mapArgs.erase("-experimentalfeatures"); + + // Revert to default + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); +} + + /* * This test covers storing encrypted zkeys in the wallet. */ diff --git a/src/wallet/asyncrpcoperation_sendmany.cpp b/src/wallet/asyncrpcoperation_sendmany.cpp index 0984b5e21..64b1a5f26 100644 --- a/src/wallet/asyncrpcoperation_sendmany.cpp +++ b/src/wallet/asyncrpcoperation_sendmany.cpp @@ -386,7 +386,17 @@ bool AsyncRPCOperation_sendmany::main_impl() { expsk = sk.expsk; ovk = expsk.full_viewing_key().ovk; } else { - // TODO: Set "from" to something! + // Sending from a t-address, which we don't have an ovk for. Instead, + // generate a common one from the HD seed. This ensures the data is + // recoverable, while keeping it logically separate from the ZIP 32 + // Sapling key hierarchy, which the user might not be using. + HDSeed seed; + if (!pwalletMain->GetHDSeed(seed)) { + throw JSONRPCError( + RPC_WALLET_ERROR, + "CWallet::GenerateNewSaplingZKey(): HD seed not found"); + } + ovk = ovkForShieldingFromTaddr(seed); } // Set change address if we are using transparent funds diff --git a/src/zcash/zip32.cpp b/src/zcash/zip32.cpp index cdad98aa9..15478843e 100644 --- a/src/zcash/zip32.cpp +++ b/src/zcash/zip32.cpp @@ -8,6 +8,7 @@ #include "random.h" #include "streams.h" #include "version.h" +#include "zcash/prf.h" #include #include @@ -15,6 +16,9 @@ const unsigned char ZCASH_HD_SEED_FP_PERSONAL[crypto_generichash_blake2b_PERSONALBYTES] = {'Z', 'c', 'a', 's', 'h', '_', 'H', 'D', '_', 'S', 'e', 'e', 'd', '_', 'F', 'P'}; +const unsigned char ZCASH_TADDR_OVK_PERSONAL[crypto_generichash_blake2b_PERSONALBYTES] = + {'Z', 'c', 'T', 'a', 'd', 'd', 'r', 'T', 'o', 'S', 'a', 'p', 'l', 'i', 'n', 'g'}; + HDSeed HDSeed::Random(size_t len) { assert(len >= 32); @@ -30,6 +34,29 @@ uint256 HDSeed::Fingerprint() const return h.GetHash(); } +uint256 ovkForShieldingFromTaddr(HDSeed& seed) { + auto rawSeed = seed.RawSeed(); + + // I = BLAKE2b-512("ZcTaddrToSapling", seed) + crypto_generichash_blake2b_state state; + assert(crypto_generichash_blake2b_init_salt_personal( + &state, + NULL, 0, // No key. + 64, + NULL, // No salt. + ZCASH_TADDR_OVK_PERSONAL) == 0); + crypto_generichash_blake2b_update(&state, rawSeed.data(), rawSeed.size()); + auto intermediate = std::array(); + crypto_generichash_blake2b_final(&state, intermediate.data(), 64); + + // I_L = I[0..32] + uint256 intermediate_L; + memcpy(intermediate_L.begin(), intermediate.data(), 32); + + // ovk = truncate_32(PRF^expand(I_L, [0x02])) + return PRF_ovk(intermediate_L); +} + namespace libzcash { boost::optional SaplingExtendedFullViewingKey::Derive(uint32_t i) const diff --git a/src/zcash/zip32.h b/src/zcash/zip32.h index fcd16b4e1..0fc5785bd 100644 --- a/src/zcash/zip32.h +++ b/src/zcash/zip32.h @@ -42,6 +42,9 @@ public: } }; +// This is not part of ZIP 32, but is here because it's linked to the HD seed. +uint256 ovkForShieldingFromTaddr(HDSeed& seed); + namespace libzcash { typedef blob88 diversifier_index_t;