From 3466b4677e3f3f868d397b3d341d70482d2a4317 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 30 Jul 2018 11:03:29 +0100 Subject: [PATCH] TransactionBuilder: Add support for transparent inputs and outputs --- src/gtest/test_transaction_builder.cpp | 98 ++++++++++++++++++++++++-- src/transaction_builder.cpp | 76 +++++++++++++++++++- src/transaction_builder.h | 21 +++++- 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp index 90cfef008..939ec2881 100644 --- a/src/gtest/test_transaction_builder.cpp +++ b/src/gtest/test_transaction_builder.cpp @@ -1,13 +1,17 @@ #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) { SelectParams(CBaseChainParams::REGTEST); @@ -15,6 +19,11 @@ TEST(TransactionBuilder, Invoke) 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(); @@ -26,19 +35,20 @@ TEST(TransactionBuilder, Invoke) auto pk = *ivk.address(d); // Create a shielding transaction from transparent to Sapling - // TODO: Add transparent inputs :P - auto builder1 = TransactionBuilder(consensusParams, 1); - builder1.AddSaplingOutput(fvk_from, pk, 50, {}); + // 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(), 0); + 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, -50); + EXPECT_EQ(tx1.valueBalance, -40000); CValidationState state; EXPECT_TRUE(ContextualCheckTransaction(tx1, state, 2, 0)); @@ -57,12 +67,13 @@ TEST(TransactionBuilder, Invoke) 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 ZEC change auto builder2 = TransactionBuilder(consensusParams, 2); ASSERT_TRUE(builder2.AddSaplingSpend(xsk, note, anchor, witness)); // Check that trying to add a different anchor fails ASSERT_FALSE(builder2.AddSaplingSpend(xsk, note, uint256(), witness)); - builder2.AddSaplingOutput(fvk, pk, 25, {}); + builder2.AddSaplingOutput(fvk, pk, 25000, {}); auto maybe_tx2 = builder2.Build(); ASSERT_EQ(static_cast(maybe_tx2), true); auto tx2 = maybe_tx2.get(); @@ -72,7 +83,7 @@ TEST(TransactionBuilder, Invoke) EXPECT_EQ(tx2.vjoinsplit.size(), 0); EXPECT_EQ(tx2.vShieldedSpend.size(), 1); EXPECT_EQ(tx2.vShieldedOutput.size(), 1); - EXPECT_EQ(tx2.valueBalance, 25); + EXPECT_EQ(tx2.valueBalance, 15000); EXPECT_TRUE(ContextualCheckTransaction(tx2, state, 3, 0)); EXPECT_EQ(state.GetRejectReason(), ""); @@ -81,3 +92,76 @@ TEST(TransactionBuilder, Invoke) 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, 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 xsk = 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(xsk, 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); +} diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index 0a6add6e7..25e1e2b17 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -5,7 +5,8 @@ #include "transaction_builder.h" #include "main.h" -#include "script/script.h" +#include "pubkey.h" +#include "script/sign.h" #include #include @@ -21,7 +22,8 @@ SpendDescriptionInfo::SpendDescriptionInfo( TransactionBuilder::TransactionBuilder( const Consensus::Params& consensusParams, - int nHeight) : consensusParams(consensusParams), nHeight(nHeight) + int nHeight, + CKeyStore* keystore) : consensusParams(consensusParams), nHeight(nHeight), keystore(keystore) { mtx = CreateNewContextualCMutableTransaction(consensusParams, nHeight); } @@ -55,8 +57,55 @@ void TransactionBuilder::AddSaplingOutput( 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; +} + boost::optional TransactionBuilder::Build() { + // Fixed fee + const CAmount fee = 10000; + + // + // 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; + } + + // TODO: Create change output (currently, the change is added to the fee) + + // + // Sapling spends and outputs + // + auto ctx = librustzcash_sapling_proving_ctx_init(); // Create Sapling SpendDescriptions @@ -141,7 +190,10 @@ boost::optional TransactionBuilder::Build() mtx.vShieldedOutput.push_back(odesc); } - // Calculate SignatureHash + // + // Signatures + // + auto consensusBranchId = CurrentEpochBranchId(nHeight, consensusParams); // Empty output script. @@ -169,5 +221,23 @@ boost::optional TransactionBuilder::Build() 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 index 30567b9e0..42ce3147d 100644 --- a/src/transaction_builder.h +++ b/src/transaction_builder.h @@ -6,7 +6,10 @@ #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" @@ -40,18 +43,29 @@ struct OutputDescriptionInfo { 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; std::vector spends; std::vector outputs; + std::vector tIns; public: - TransactionBuilder(const Consensus::Params& consensusParams, int nHeight); + TransactionBuilder(const Consensus::Params& consensusParams, int nHeight, CKeyStore* keyStore = nullptr); // Returns false if the anchor does not match the anchor used by // previously-added Sapling spends. @@ -67,6 +81,11 @@ public: 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); + boost::optional Build(); };