diff --git a/depends/packages/crate_sapling_crypto.mk b/depends/packages/crate_sapling_crypto.mk index e1ef857c9..2da5e2406 100644 --- a/depends/packages/crate_sapling_crypto.mk +++ b/depends/packages/crate_sapling_crypto.mk @@ -3,8 +3,8 @@ $(package)_crate_name=sapling-crypto $(package)_download_path=https://github.com/zcash-hackworks/$($(package)_crate_name)/archive/ $(package)_file_name=$(package)-$($(package)_git_commit).tar.gz $(package)_download_file=$($(package)_git_commit).tar.gz -$(package)_sha256_hash=480ffc31b399a9517178289799f9db01152bedbc534a6a2d2dbac245421fb6fe -$(package)_git_commit=6abfcca25ae233922ecc18a4d2d0b5cb7aab7c8c +$(package)_sha256_hash=ae3a122b1f1ce97b4e80e0e8542e19aa1516e99e6c72875688c886af1a881558 +$(package)_git_commit=21084bde2019c04bd34208e63c3560fe2c02fb0e $(package)_crate_versioned_name=$($(package)_crate_name) define $(package)_preprocess_cmds diff --git a/depends/packages/librustzcash.mk b/depends/packages/librustzcash.mk index 3a338cc21..aeff4d890 100644 --- a/depends/packages/librustzcash.mk +++ b/depends/packages/librustzcash.mk @@ -3,8 +3,8 @@ $(package)_version=0.1 $(package)_download_path=https://github.com/zcash/$(package)/archive/ $(package)_file_name=$(package)-$($(package)_git_commit).tar.gz $(package)_download_file=$($(package)_git_commit).tar.gz -$(package)_sha256_hash=5a50aae38a9ef4823cd319278ad95706a129cc091e1cca9342802f1ff75aba15 -$(package)_git_commit=93e26d1d8716ac88f8bb372d442315edcd2deabd +$(package)_sha256_hash=1700c1699552f3aa4db8de265399bc6606ae976b6741f7bbeb0b89f24abf9f7f +$(package)_git_commit=f55a654901dc6278dd5738f06f2e70263fccd96b $(package)_dependencies=rust $(rust_crates) $(package)_patches=cargo.config diff --git a/depends/patches/librustzcash/cargo.config b/depends/patches/librustzcash/cargo.config index 234a9c97b..7696632a1 100644 --- a/depends/patches/librustzcash/cargo.config +++ b/depends/patches/librustzcash/cargo.config @@ -8,7 +8,7 @@ replace-with = "vendored-sources" [source."https://github.com/zcash-hackworks/sapling-crypto"] git = "https://github.com/zcash-hackworks/sapling-crypto" -rev = "6abfcca25ae233922ecc18a4d2d0b5cb7aab7c8c" +rev = "21084bde2019c04bd34208e63c3560fe2c02fb0e" replace-with = "vendored-sources" [source.vendored-sources] diff --git a/src/Makefile.am b/src/Makefile.am index cf5cc46cf..804bc4865 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -199,6 +199,7 @@ BITCOIN_CORE_H = \ timedata.h \ tinyformat.h \ torcontrol.h \ + transaction_builder.h \ txdb.h \ txmempool.h \ ui_interface.h \ @@ -384,6 +385,7 @@ libbitcoin_common_a_SOURCES = \ script/script_error.cpp \ script/sign.cpp \ script/standard.cpp \ + transaction_builder.cpp \ $(BITCOIN_CORE_H) \ $(LIBZCASH_H) diff --git a/src/Makefile.gtest.include b/src/Makefile.gtest.include index 519fe1552..e089d5783 100644 --- a/src/Makefile.gtest.include +++ b/src/Makefile.gtest.include @@ -34,6 +34,7 @@ zcash_gtest_SOURCES += \ gtest/test_rpc.cpp \ gtest/test_sapling_note.cpp \ gtest/test_transaction.cpp \ + gtest/test_transaction_builder.cpp \ gtest/test_upgrades.cpp \ gtest/test_validation.cpp \ gtest/test_circuit.cpp \ diff --git a/src/gtest/main.cpp b/src/gtest/main.cpp index 6576edc03..f3db2a4bb 100644 --- a/src/gtest/main.cpp +++ b/src/gtest/main.cpp @@ -1,5 +1,6 @@ #include "gmock/gmock.h" #include "crypto/common.h" +#include "key.h" #include "pubkey.h" #include "zcash/JoinSplit.hpp" #include "util.h" @@ -20,6 +21,8 @@ ZCJoinSplit* params; int main(int argc, char **argv) { assert(init_and_check_sodium() != -1); + ECC_Start(); + libsnark::default_r1cs_ppzksnark_pp::init_public_params(); libsnark::inhibit_profiling_info = true; libsnark::inhibit_profiling_counters = true; diff --git a/src/gtest/test_paymentdisclosure.cpp b/src/gtest/test_paymentdisclosure.cpp index d4cefa229..89be17dae 100644 --- a/src/gtest/test_paymentdisclosure.cpp +++ b/src/gtest/test_paymentdisclosure.cpp @@ -93,7 +93,6 @@ public: // Note that the zpd: prefix is not part of the payment disclosure blob itself. It is only // used as convention to improve the user experience when sharing payment disclosure blobs. TEST(paymentdisclosure, mainnet) { - ECC_Start(); SelectParams(CBaseChainParams::MAIN); boost::filesystem::path pathTemp = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); diff --git a/src/gtest/test_sapling_note.cpp b/src/gtest/test_sapling_note.cpp index 06f2d6188..3e336ec7e 100644 --- a/src/gtest/test_sapling_note.cpp +++ b/src/gtest/test_sapling_note.cpp @@ -49,7 +49,7 @@ TEST(SaplingNote, TestVectors) // Test nullifier SaplingSpendingKey spendingKey(sk); - ASSERT_EQ(note.nullifier(spendingKey, note_pos), nf); + ASSERT_EQ(note.nullifier(spendingKey.full_viewing_key(), note_pos), nf); } diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp new file mode 100644 index 000000000..2ad90942b --- /dev/null +++ b/src/gtest/test_transaction_builder.cpp @@ -0,0 +1,337 @@ +#include "chainparams.h" +#include "consensus/params.h" +#include "consensus/validation.h" +#include "key_io.h" +#include "main.h" +#include "pubkey.h" +#include "transaction_builder.h" +#include "zcash/Address.hpp" + +#include +#include + +static const std::string tSecretRegtest = "cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN"; + +TEST(TransactionBuilder, Invoke) +{ + ECC_Start(); + + SelectParams(CBaseChainParams::REGTEST); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + auto consensusParams = Params().GetConsensus(); + + CBasicKeyStore keystore; + CKey tsk = DecodeSecret(tSecretRegtest); + keystore.AddKey(tsk); + auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); + + auto sk_from = libzcash::SaplingSpendingKey::random(); + auto fvk_from = sk_from.full_viewing_key(); + + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto ivk = fvk.in_viewing_key(); + libzcash::diversifier_t d = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + auto pk = *ivk.address(d); + + // Create a shielding transaction from transparent to Sapling + // 0.0005 t-ZEC in, 0.0004 z-ZEC out, 0.0001 t-ZEC fee + auto builder1 = TransactionBuilder(consensusParams, 1, &keystore); + builder1.AddTransparentInput(COutPoint(), scriptPubKey, 50000); + builder1.AddSaplingOutput(fvk_from, pk, 40000, {}); + auto maybe_tx1 = builder1.Build(); + ASSERT_EQ(static_cast(maybe_tx1), true); + auto tx1 = maybe_tx1.get(); + + EXPECT_EQ(tx1.vin.size(), 1); + EXPECT_EQ(tx1.vout.size(), 0); + EXPECT_EQ(tx1.vjoinsplit.size(), 0); + EXPECT_EQ(tx1.vShieldedSpend.size(), 0); + EXPECT_EQ(tx1.vShieldedOutput.size(), 1); + EXPECT_EQ(tx1.valueBalance, -40000); + + CValidationState state; + EXPECT_TRUE(ContextualCheckTransaction(tx1, state, 2, 0)); + EXPECT_EQ(state.GetRejectReason(), ""); + + // Prepare to spend the note that was just created + auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( + tx1.vShieldedOutput[0].encCiphertext, ivk, tx1.vShieldedOutput[0].ephemeralKey); + ASSERT_EQ(static_cast(maybe_pt), true); + auto maybe_note = maybe_pt.get().note(ivk); + ASSERT_EQ(static_cast(maybe_note), true); + auto note = maybe_note.get(); + ZCSaplingIncrementalMerkleTree tree; + tree.append(tx1.vShieldedOutput[0].cm); + auto anchor = tree.root(); + auto witness = tree.witness(); + + // Create a Sapling-only transaction + // 0.0004 z-ZEC in, 0.00025 z-ZEC out, 0.0001 t-ZEC fee, 0.00005 z-ZEC change + auto builder2 = TransactionBuilder(consensusParams, 2); + ASSERT_TRUE(builder2.AddSaplingSpend(expsk, note, anchor, witness)); + // Check that trying to add a different anchor fails + ASSERT_FALSE(builder2.AddSaplingSpend(expsk, note, uint256(), witness)); + + builder2.AddSaplingOutput(fvk, pk, 25000, {}); + auto maybe_tx2 = builder2.Build(); + ASSERT_EQ(static_cast(maybe_tx2), true); + auto tx2 = maybe_tx2.get(); + + EXPECT_EQ(tx2.vin.size(), 0); + EXPECT_EQ(tx2.vout.size(), 0); + EXPECT_EQ(tx2.vjoinsplit.size(), 0); + EXPECT_EQ(tx2.vShieldedSpend.size(), 1); + EXPECT_EQ(tx2.vShieldedOutput.size(), 2); + EXPECT_EQ(tx2.valueBalance, 10000); + + EXPECT_TRUE(ContextualCheckTransaction(tx2, state, 3, 0)); + EXPECT_EQ(state.GetRejectReason(), ""); + + // Revert to default + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); +} + +TEST(TransactionBuilder, ThrowsOnTransparentInputWithoutKeyStore) +{ + auto consensusParams = Params().GetConsensus(); + + auto builder = TransactionBuilder(consensusParams, 1); + ASSERT_THROW(builder.AddTransparentInput(COutPoint(), CScript(), 1), std::runtime_error); +} + +TEST(TransactionBuilder, RejectsInvalidTransparentOutput) +{ + auto consensusParams = Params().GetConsensus(); + + // Default CTxDestination type is an invalid address + CTxDestination taddr; + auto builder = TransactionBuilder(consensusParams, 1); + EXPECT_FALSE(builder.AddTransparentOutput(taddr, 50)); +} + +TEST(TransactionBuilder, RejectsInvalidTransparentChangeAddress) +{ + auto consensusParams = Params().GetConsensus(); + + // Default CTxDestination type is an invalid address + CTxDestination taddr; + auto builder = TransactionBuilder(consensusParams, 1); + EXPECT_FALSE(builder.SendChangeTo(taddr)); +} + +TEST(TransactionBuilder, FailsWithNegativeChange) +{ + SelectParams(CBaseChainParams::REGTEST); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + auto consensusParams = Params().GetConsensus(); + + // Generate dummy Sapling address + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + + // Set up dummy transparent address + CBasicKeyStore keystore; + CKey tsk = DecodeSecret(tSecretRegtest); + keystore.AddKey(tsk); + auto tkeyid = tsk.GetPubKey().GetID(); + auto scriptPubKey = GetScriptForDestination(tkeyid); + CTxDestination taddr = tkeyid; + + // Generate dummy Sapling note + libzcash::SaplingNote note(pk, 59999); + auto cm = note.cm().value(); + ZCSaplingIncrementalMerkleTree tree; + tree.append(cm); + auto anchor = tree.root(); + auto witness = tree.witness(); + + // Fail if there is only a Sapling output + // 0.0005 z-ZEC out, 0.0001 t-ZEC fee + auto builder = TransactionBuilder(consensusParams, 1); + builder.AddSaplingOutput(fvk, pk, 50000, {}); + EXPECT_FALSE(static_cast(builder.Build())); + + // Fail if there is only a transparent output + // 0.0005 t-ZEC out, 0.0001 t-ZEC fee + builder = TransactionBuilder(consensusParams, 1, &keystore); + EXPECT_TRUE(builder.AddTransparentOutput(taddr, 50000)); + EXPECT_FALSE(static_cast(builder.Build())); + + // Fails if there is insufficient input + // 0.0005 t-ZEC out, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in + EXPECT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); + EXPECT_FALSE(static_cast(builder.Build())); + + // Succeeds if there is sufficient input + builder.AddTransparentInput(COutPoint(), scriptPubKey, 1); + EXPECT_TRUE(static_cast(builder.Build())); + + // Revert to default + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); +} + +TEST(TransactionBuilder, ChangeOutput) +{ + SelectParams(CBaseChainParams::REGTEST); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + auto consensusParams = Params().GetConsensus(); + + // Generate dummy Sapling address + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto pk = sk.default_address(); + + // Generate dummy Sapling note + libzcash::SaplingNote note(pk, 25000); + auto cm = note.cm().value(); + ZCSaplingIncrementalMerkleTree tree; + tree.append(cm); + auto anchor = tree.root(); + auto witness = tree.witness(); + + // Generate change Sapling address + auto sk2 = libzcash::SaplingSpendingKey::random(); + auto fvkOut = sk2.full_viewing_key(); + auto zChangeAddr = sk2.default_address(); + + // Set up dummy transparent address + CBasicKeyStore keystore; + CKey tsk = DecodeSecret(tSecretRegtest); + keystore.AddKey(tsk); + auto tkeyid = tsk.GetPubKey().GetID(); + auto scriptPubKey = GetScriptForDestination(tkeyid); + CTxDestination taddr = tkeyid; + + // No change address and no Sapling spends + { + auto builder = TransactionBuilder(consensusParams, 1, &keystore); + builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); + EXPECT_FALSE(static_cast(builder.Build())); + } + + // Change to the same address as the first Sapling spend + { + auto builder = TransactionBuilder(consensusParams, 1, &keystore); + builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); + ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); + auto maybe_tx = builder.Build(); + ASSERT_EQ(static_cast(maybe_tx), true); + auto tx = maybe_tx.get(); + + EXPECT_EQ(tx.vin.size(), 1); + EXPECT_EQ(tx.vout.size(), 0); + EXPECT_EQ(tx.vjoinsplit.size(), 0); + EXPECT_EQ(tx.vShieldedSpend.size(), 1); + EXPECT_EQ(tx.vShieldedOutput.size(), 1); + EXPECT_EQ(tx.valueBalance, -15000); + } + + // Change to a Sapling address + { + auto builder = TransactionBuilder(consensusParams, 1, &keystore); + builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); + builder.SendChangeTo(zChangeAddr, fvkOut); + auto maybe_tx = builder.Build(); + ASSERT_EQ(static_cast(maybe_tx), true); + auto tx = maybe_tx.get(); + + EXPECT_EQ(tx.vin.size(), 1); + EXPECT_EQ(tx.vout.size(), 0); + EXPECT_EQ(tx.vjoinsplit.size(), 0); + EXPECT_EQ(tx.vShieldedSpend.size(), 0); + EXPECT_EQ(tx.vShieldedOutput.size(), 1); + EXPECT_EQ(tx.valueBalance, -15000); + } + + // Change to a transparent address + { + auto builder = TransactionBuilder(consensusParams, 1, &keystore); + builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); + ASSERT_TRUE(builder.SendChangeTo(taddr)); + auto maybe_tx = builder.Build(); + ASSERT_EQ(static_cast(maybe_tx), true); + auto tx = maybe_tx.get(); + + EXPECT_EQ(tx.vin.size(), 1); + EXPECT_EQ(tx.vout.size(), 1); + EXPECT_EQ(tx.vjoinsplit.size(), 0); + EXPECT_EQ(tx.vShieldedSpend.size(), 0); + EXPECT_EQ(tx.vShieldedOutput.size(), 0); + EXPECT_EQ(tx.valueBalance, 0); + EXPECT_EQ(tx.vout[0].nValue, 15000); + } + + // Revert to default + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); +} + +TEST(TransactionBuilder, SetFee) +{ + SelectParams(CBaseChainParams::REGTEST); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); + auto consensusParams = Params().GetConsensus(); + + // Generate dummy Sapling address + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + + // Generate dummy Sapling note + libzcash::SaplingNote note(pk, 50000); + auto cm = note.cm().value(); + ZCSaplingIncrementalMerkleTree tree; + tree.append(cm); + auto anchor = tree.root(); + auto witness = tree.witness(); + + // Default fee + { + auto builder = TransactionBuilder(consensusParams, 1); + ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); + builder.AddSaplingOutput(fvk, pk, 25000, {}); + auto maybe_tx = builder.Build(); + ASSERT_EQ(static_cast(maybe_tx), true); + auto tx = maybe_tx.get(); + + EXPECT_EQ(tx.vin.size(), 0); + EXPECT_EQ(tx.vout.size(), 0); + EXPECT_EQ(tx.vjoinsplit.size(), 0); + EXPECT_EQ(tx.vShieldedSpend.size(), 1); + EXPECT_EQ(tx.vShieldedOutput.size(), 2); + EXPECT_EQ(tx.valueBalance, 10000); + } + + // Configured fee + { + auto builder = TransactionBuilder(consensusParams, 1); + ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); + builder.AddSaplingOutput(fvk, pk, 25000, {}); + builder.SetFee(20000); + auto maybe_tx = builder.Build(); + ASSERT_EQ(static_cast(maybe_tx), true); + auto tx = maybe_tx.get(); + + EXPECT_EQ(tx.vin.size(), 0); + EXPECT_EQ(tx.vout.size(), 0); + EXPECT_EQ(tx.vjoinsplit.size(), 0); + EXPECT_EQ(tx.vShieldedSpend.size(), 1); + EXPECT_EQ(tx.vShieldedOutput.size(), 2); + EXPECT_EQ(tx.valueBalance, 20000); + } + + // Revert to default + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); +} diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp new file mode 100644 index 000000000..f2b915c5a --- /dev/null +++ b/src/transaction_builder.cpp @@ -0,0 +1,283 @@ +// Copyright (c) 2018 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "transaction_builder.h" + +#include "main.h" +#include "pubkey.h" +#include "script/sign.h" + +#include +#include + +SpendDescriptionInfo::SpendDescriptionInfo( + libzcash::SaplingExpandedSpendingKey expsk, + libzcash::SaplingNote note, + uint256 anchor, + ZCSaplingIncrementalWitness witness) : expsk(expsk), note(note), anchor(anchor), witness(witness) +{ + librustzcash_sapling_generate_r(alpha.begin()); +} + +TransactionBuilder::TransactionBuilder( + const Consensus::Params& consensusParams, + int nHeight, + CKeyStore* keystore) : consensusParams(consensusParams), nHeight(nHeight), keystore(keystore) +{ + mtx = CreateNewContextualCMutableTransaction(consensusParams, nHeight); +} + +bool TransactionBuilder::AddSaplingSpend( + libzcash::SaplingExpandedSpendingKey expsk, + libzcash::SaplingNote note, + uint256 anchor, + ZCSaplingIncrementalWitness witness) +{ + // Consistency check: all anchors must equal the first one + if (!spends.empty()) { + if (spends[0].anchor != anchor) { + return false; + } + } + + spends.emplace_back(expsk, note, anchor, witness); + mtx.valueBalance += note.value(); + return true; +} + +void TransactionBuilder::AddSaplingOutput( + libzcash::SaplingFullViewingKey from, + libzcash::SaplingPaymentAddress to, + CAmount value, + std::array memo) +{ + auto note = libzcash::SaplingNote(to, value); + outputs.emplace_back(from.ovk, note, memo); + mtx.valueBalance -= value; +} + +void TransactionBuilder::AddTransparentInput(COutPoint utxo, CScript scriptPubKey, CAmount value) +{ + if (keystore == nullptr) { + throw std::runtime_error("Cannot add transparent inputs to a TransactionBuilder without a keystore"); + } + + mtx.vin.emplace_back(utxo); + tIns.emplace_back(scriptPubKey, value); +} + +bool TransactionBuilder::AddTransparentOutput(CTxDestination& to, CAmount value) +{ + if (!IsValidDestination(to)) { + return false; + } + + CScript scriptPubKey = GetScriptForDestination(to); + CTxOut out(value, scriptPubKey); + mtx.vout.push_back(out); + return true; +} + +void TransactionBuilder::SetFee(CAmount fee) +{ + this->fee = fee; +} + +void TransactionBuilder::SendChangeTo(libzcash::SaplingPaymentAddress changeAddr, libzcash::SaplingFullViewingKey fvkOut) +{ + zChangeAddr = std::make_pair(fvkOut, changeAddr); + tChangeAddr = boost::none; +} + +bool TransactionBuilder::SendChangeTo(CTxDestination& changeAddr) +{ + if (!IsValidDestination(changeAddr)) { + return false; + } + + tChangeAddr = changeAddr; + zChangeAddr = boost::none; + + return true; +} + +boost::optional TransactionBuilder::Build() +{ + // + // Consistency checks + // + + // Valid change + CAmount change = mtx.valueBalance - fee; + for (auto tIn : tIns) { + change += tIn.value; + } + for (auto tOut : mtx.vout) { + change -= tOut.nValue; + } + if (change < 0) { + return boost::none; + } + + // + // Change output + // + + if (change > 0) { + // Send change to the specified change address. If no change address + // was set, send change to the first Sapling address given as input. + if (zChangeAddr) { + AddSaplingOutput(zChangeAddr->first, zChangeAddr->second, change, {}); + } else if (tChangeAddr) { + // tChangeAddr has already been validated. + assert(AddTransparentOutput(tChangeAddr.value(), change)); + } else if (!spends.empty()) { + auto fvk = spends[0].expsk.full_viewing_key(); + auto note = spends[0].note; + libzcash::SaplingPaymentAddress changeAddr(note.d, note.pk_d); + AddSaplingOutput(fvk, changeAddr, change, {}); + } else { + return boost::none; + } + } + + // + // Sapling spends and outputs + // + + auto ctx = librustzcash_sapling_proving_ctx_init(); + + // Create Sapling SpendDescriptions + for (auto spend : spends) { + auto cm = spend.note.cm(); + auto nf = spend.note.nullifier( + spend.expsk.full_viewing_key(), spend.witness.position()); + if (!(cm && nf)) { + librustzcash_sapling_proving_ctx_free(ctx); + return boost::none; + } + + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << spend.witness.path(); + std::vector witness(ss.begin(), ss.end()); + + SpendDescription sdesc; + if (!librustzcash_sapling_spend_proof( + ctx, + spend.expsk.full_viewing_key().ak.begin(), + spend.expsk.nsk.begin(), + spend.note.d.data(), + spend.note.r.begin(), + spend.alpha.begin(), + spend.note.value(), + spend.anchor.begin(), + witness.data(), + sdesc.cv.begin(), + sdesc.rk.begin(), + sdesc.zkproof.data())) { + librustzcash_sapling_proving_ctx_free(ctx); + return boost::none; + } + + sdesc.anchor = spend.anchor; + sdesc.nullifier = *nf; + mtx.vShieldedSpend.push_back(sdesc); + } + + // Create Sapling OutputDescriptions + for (auto output : outputs) { + auto cm = output.note.cm(); + if (!cm) { + librustzcash_sapling_proving_ctx_free(ctx); + return boost::none; + } + + libzcash::SaplingNotePlaintext notePlaintext(output.note, output.memo); + + auto res = notePlaintext.encrypt(output.note.pk_d); + if (!res) { + librustzcash_sapling_proving_ctx_free(ctx); + return boost::none; + } + auto enc = res.get(); + auto encryptor = enc.second; + + OutputDescription odesc; + if (!librustzcash_sapling_output_proof( + ctx, + encryptor.get_esk().begin(), + output.note.d.data(), + output.note.pk_d.begin(), + output.note.r.begin(), + output.note.value(), + odesc.cv.begin(), + odesc.zkproof.begin())) { + librustzcash_sapling_proving_ctx_free(ctx); + return boost::none; + } + + odesc.cm = *cm; + odesc.ephemeralKey = encryptor.get_epk(); + odesc.encCiphertext = enc.first; + + libzcash::SaplingOutgoingPlaintext outPlaintext(output.note.pk_d, encryptor.get_esk()); + odesc.outCiphertext = outPlaintext.encrypt( + output.ovk, + odesc.cv, + odesc.cm, + encryptor); + mtx.vShieldedOutput.push_back(odesc); + } + + // + // Signatures + // + + auto consensusBranchId = CurrentEpochBranchId(nHeight, consensusParams); + + // Empty output script. + uint256 dataToBeSigned; + CScript scriptCode; + try { + dataToBeSigned = SignatureHash(scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId); + } catch (std::logic_error ex) { + librustzcash_sapling_proving_ctx_free(ctx); + return boost::none; + } + + // Create Sapling spendAuth and binding signatures + for (size_t i = 0; i < spends.size(); i++) { + librustzcash_sapling_spend_sig( + spends[i].expsk.ask.begin(), + spends[i].alpha.begin(), + dataToBeSigned.begin(), + mtx.vShieldedSpend[i].spendAuthSig.data()); + } + librustzcash_sapling_binding_sig( + ctx, + mtx.valueBalance, + dataToBeSigned.begin(), + mtx.bindingSig.data()); + + librustzcash_sapling_proving_ctx_free(ctx); + + // Transparent signatures + CTransaction txNewConst(mtx); + for (int nIn = 0; nIn < mtx.vin.size(); nIn++) { + auto tIn = tIns[nIn]; + SignatureData sigdata; + bool signSuccess = ProduceSignature( + TransactionSignatureCreator( + keystore, &txNewConst, nIn, tIn.value, SIGHASH_ALL), + tIn.scriptPubKey, sigdata, consensusBranchId); + + if (!signSuccess) { + return boost::none; + } else { + UpdateTransaction(mtx, nIn, sigdata); + } + } + + return CTransaction(mtx); +} diff --git a/src/transaction_builder.h b/src/transaction_builder.h new file mode 100644 index 000000000..6c159e7aa --- /dev/null +++ b/src/transaction_builder.h @@ -0,0 +1,102 @@ +// Copyright (c) 2018 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef TRANSACTION_BUILDER_H +#define TRANSACTION_BUILDER_H + +#include "consensus/params.h" +#include "keystore.h" +#include "primitives/transaction.h" +#include "script/script.h" +#include "script/standard.h" +#include "uint256.h" +#include "zcash/Address.hpp" +#include "zcash/IncrementalMerkleTree.hpp" +#include "zcash/Note.hpp" +#include "zcash/NoteEncryption.hpp" + +#include + +struct SpendDescriptionInfo { + libzcash::SaplingExpandedSpendingKey expsk; + libzcash::SaplingNote note; + uint256 alpha; + uint256 anchor; + ZCSaplingIncrementalWitness witness; + + SpendDescriptionInfo( + libzcash::SaplingExpandedSpendingKey expsk, + libzcash::SaplingNote note, + uint256 anchor, + ZCSaplingIncrementalWitness witness); +}; + +struct OutputDescriptionInfo { + uint256 ovk; + libzcash::SaplingNote note; + std::array memo; + + OutputDescriptionInfo( + uint256 ovk, + libzcash::SaplingNote note, + std::array memo) : ovk(ovk), note(note), memo(memo) {} +}; + +struct TransparentInputInfo { + CScript scriptPubKey; + CAmount value; + + TransparentInputInfo( + CScript scriptPubKey, + CAmount value) : scriptPubKey(scriptPubKey), value(value) {} +}; + +class TransactionBuilder +{ +private: + Consensus::Params consensusParams; + int nHeight; + const CKeyStore* keystore; + CMutableTransaction mtx; + CAmount fee = 10000; + + std::vector spends; + std::vector outputs; + std::vector tIns; + + boost::optional> zChangeAddr; + boost::optional tChangeAddr; + +public: + TransactionBuilder(const Consensus::Params& consensusParams, int nHeight, CKeyStore* keyStore = nullptr); + + void SetFee(CAmount fee); + + // Returns false if the anchor does not match the anchor used by + // previously-added Sapling spends. + bool AddSaplingSpend( + libzcash::SaplingExpandedSpendingKey expsk, + libzcash::SaplingNote note, + uint256 anchor, + ZCSaplingIncrementalWitness witness); + + void AddSaplingOutput( + libzcash::SaplingFullViewingKey from, + libzcash::SaplingPaymentAddress to, + CAmount value, + std::array memo); + + // Assumes that the value correctly corresponds to the provided UTXO. + void AddTransparentInput(COutPoint utxo, CScript scriptPubKey, CAmount value); + + bool AddTransparentOutput(CTxDestination& to, CAmount value); + + void SendChangeTo(libzcash::SaplingPaymentAddress changeAddr, libzcash::SaplingFullViewingKey fvkOut); + + bool SendChangeTo(CTxDestination& changeAddr); + + boost::optional Build(); +}; + +#endif /* TRANSACTION_BUILDER_H */ diff --git a/src/wallet/gtest/test_wallet_zkeys.cpp b/src/wallet/gtest/test_wallet_zkeys.cpp index 317e4ff54..9fe107ccc 100644 --- a/src/wallet/gtest/test_wallet_zkeys.cpp +++ b/src/wallet/gtest/test_wallet_zkeys.cpp @@ -259,8 +259,6 @@ TEST(wallet_zkeys_tests, WriteViewingKeyDirectToDB) { * This test covers methods on CWalletDB to load/save crypted z keys. */ TEST(wallet_zkeys_tests, write_cryptedzkey_direct_to_db) { - ECC_Start(); - SelectParams(CBaseChainParams::TESTNET); // Get temporary and unique path for file. diff --git a/src/zcash/IncrementalMerkleTree.hpp b/src/zcash/IncrementalMerkleTree.hpp index 912e9fff0..1e4c8ac85 100644 --- a/src/zcash/IncrementalMerkleTree.hpp +++ b/src/zcash/IncrementalMerkleTree.hpp @@ -170,6 +170,10 @@ public: return tree.last(); } + uint64_t position() const { + return tree.size() - 1; + } + Hash root() const { return tree.root(Depth, partial_path()); } diff --git a/src/zcash/Note.cpp b/src/zcash/Note.cpp index 1108e1db5..c6c72e297 100644 --- a/src/zcash/Note.cpp +++ b/src/zcash/Note.cpp @@ -65,9 +65,8 @@ boost::optional SaplingNote::cm() const { } // Call librustzcash to compute the nullifier -boost::optional SaplingNote::nullifier(const SaplingSpendingKey& sk, const uint64_t position) const +boost::optional SaplingNote::nullifier(const SaplingFullViewingKey& vk, const uint64_t position) const { - auto vk = sk.full_viewing_key(); auto ak = vk.ak; auto nk = vk.nk; diff --git a/src/zcash/Note.hpp b/src/zcash/Note.hpp index 5a3e55dfe..f1b8e4323 100644 --- a/src/zcash/Note.hpp +++ b/src/zcash/Note.hpp @@ -57,7 +57,7 @@ public: virtual ~SaplingNote() {}; boost::optional cm() const; - boost::optional nullifier(const SaplingSpendingKey &sk, const uint64_t position) const; + boost::optional nullifier(const SaplingFullViewingKey &vk, const uint64_t position) const; }; class BaseNotePlaintext {