From 2da0856e6fffdb2c6f1a44a5a5c073a1de4b91e5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 11 Feb 2022 23:41:35 +0000 Subject: [PATCH] Add Orchard recipient support to the transaction builder Co-authored-by: Kris Nuttycombe --- .cargo/config.offline | 7 +- Cargo.lock | 19 +- Cargo.toml | 11 +- src/gtest/test_dynamicusage.cpp | 10 +- src/gtest/test_mempoollimit.cpp | 6 +- src/gtest/test_transaction_builder.cpp | 85 +++++++-- src/gtest/test_validation.cpp | 2 +- src/primitives/orchard.h | 6 +- src/rust/include/rust/builder.h | 80 +++++++++ src/rust/include/rust/orchard.h | 1 + src/rust/src/builder_ffi.rs | 169 ++++++++++++++++++ src/rust/src/rustzcash.rs | 1 + src/rust/src/transaction_ffi.rs | 4 +- src/transaction_builder.cpp | 139 ++++++++++++-- src/transaction_builder.h | 154 +++++++++++++++- src/utiltest.cpp | 3 +- .../asyncrpcoperation_mergetoaddress.cpp | 2 +- .../asyncrpcoperation_saplingmigration.cpp | 2 +- src/wallet/asyncrpcoperation_sendmany.cpp | 2 +- .../asyncrpcoperation_shieldcoinbase.cpp | 2 +- src/wallet/gtest/test_wallet.cpp | 24 +-- src/wallet/rpcwallet.cpp | 21 ++- src/wallet/test/rpc_wallet_tests.cpp | 18 +- src/zcash/address/orchard.hpp | 3 + 24 files changed, 684 insertions(+), 87 deletions(-) create mode 100644 src/rust/include/rust/builder.h create mode 100644 src/rust/src/builder_ffi.rs diff --git a/.cargo/config.offline b/.cargo/config.offline index 1f8659ab3..4828f3d41 100644 --- a/.cargo/config.offline +++ b/.cargo/config.offline @@ -3,7 +3,12 @@ replace-with = "vendored-sources" [source."https://github.com/zcash/librustzcash.git"] git = "https://github.com/zcash/librustzcash.git" -rev = "5622b060b1f57de7afc3d0b4e425b9b4b22482a0" +rev = "3d935a94e75786a67c3ea4992d7c372af203086f" +replace-with = "vendored-sources" + +[source."https://github.com/zcash/orchard.git"] +git = "https://github.com/zcash/orchard.git" +rev = "c4cd541e6c9ca657fa33fdd686d70d81bf3e4e65" replace-with = "vendored-sources" [source.vendored-sources] diff --git a/Cargo.lock b/Cargo.lock index ca602e3b1..b4c502212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,7 +501,7 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "blake2b_simd 1.0.0", "byteorder", @@ -510,7 +510,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.0.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "blake2b_simd 1.0.0", ] @@ -1090,8 +1090,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "orchard" version = "0.1.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31f03b6d0aee6d993cac35388b818e04f076ded0ab284979e4d7cd5a8b3c2be" +source = "git+https://github.com/zcash/orchard.git?rev=c4cd541e6c9ca657fa33fdd686d70d81bf3e4e65#c4cd541e6c9ca657fa33fdd686d70d81bf3e4e65" dependencies = [ "aes", "arrayvec 0.7.2", @@ -1917,7 +1916,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.0.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "bech32", "bs58", @@ -1928,7 +1927,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.0.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "byteorder", "nonempty", @@ -1937,7 +1936,7 @@ dependencies = [ [[package]] name = "zcash_history" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "bigint", "blake2b_simd 1.0.0", @@ -1947,7 +1946,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "chacha20", "chacha20poly1305", @@ -1958,7 +1957,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "aes", "bip0039", @@ -1994,7 +1993,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=4f4a25252f0fc6b84c44316d810107bc7ea7f32a#4f4a25252f0fc6b84c44316d810107bc7ea7f32a" +source = "git+https://github.com/zcash/librustzcash.git?rev=3d935a94e75786a67c3ea4992d7c372af203086f#3d935a94e75786a67c3ea4992d7c372af203086f" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 358c0dc8f..3ceb20ec3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,9 @@ codegen-units = 1 [patch.crates-io] hdwallet = { git = "https://github.com/nuttycom/hdwallet", rev = "576683b9f2865f1118c309017ff36e01f84420c9" } -zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" } -zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" } -zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" } -zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" } -zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "4f4a25252f0fc6b84c44316d810107bc7ea7f32a" } +orchard = { git = "https://github.com/zcash/orchard.git", rev = "c4cd541e6c9ca657fa33fdd686d70d81bf3e4e65" } +zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "3d935a94e75786a67c3ea4992d7c372af203086f" } +zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "3d935a94e75786a67c3ea4992d7c372af203086f" } +zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "3d935a94e75786a67c3ea4992d7c372af203086f" } +zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "3d935a94e75786a67c3ea4992d7c372af203086f" } +zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "3d935a94e75786a67c3ea4992d7c372af203086f" } diff --git a/src/gtest/test_dynamicusage.cpp b/src/gtest/test_dynamicusage.cpp index 0c8410b8f..f5a3048e5 100644 --- a/src/gtest/test_dynamicusage.cpp +++ b/src/gtest/test_dynamicusage.cpp @@ -11,6 +11,8 @@ #include "transaction_builder.h" #include "utiltest.h" +#include + TEST(RecursiveDynamicUsageTests, TestTransactionTransparent) { auto consensusParams = RegtestActivateSapling(); @@ -20,7 +22,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparent) auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); CTxDestination taddr = tsk.GetPubKey().GetID(); - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000); builder.AddTransparentOutput(taddr, 40000); @@ -53,7 +55,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToSapling) auto sk = libzcash::SaplingSpendingKey::random(); auto testNote = GetTestSaplingNote(sk.default_address(), 50000); - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 5000, {}); @@ -74,7 +76,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparentToSapling) auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); auto sk = libzcash::SaplingSpendingKey::random(); - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000); builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 40000, {}); @@ -96,7 +98,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToTransparent) auto sk = libzcash::SaplingSpendingKey::random(); auto testNote = GetTestSaplingNote(sk.default_address(), 50000); - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddTransparentOutput(taddr, 40000); diff --git a/src/gtest/test_mempoollimit.cpp b/src/gtest/test_mempoollimit.cpp index 0f38aaaeb..8459c3694 100644 --- a/src/gtest/test_mempoollimit.cpp +++ b/src/gtest/test_mempoollimit.cpp @@ -119,7 +119,7 @@ TEST(MempoolLimitTests, WeightedTxInfoFromTx) // Default fee { - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 25000, {}); @@ -130,7 +130,7 @@ TEST(MempoolLimitTests, WeightedTxInfoFromTx) // Lower than standard fee { - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 25000, {}); static_assert(DEFAULT_FEE == 1000); @@ -143,7 +143,7 @@ TEST(MempoolLimitTests, WeightedTxInfoFromTx) // Larger Tx { - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(sk.expanded_spending_key(), testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 5000, {}); builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 5000, {}); diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp index c17fafb7d..9e2b181b1 100644 --- a/src/gtest/test_transaction_builder.cpp +++ b/src/gtest/test_transaction_builder.cpp @@ -9,6 +9,9 @@ #include "transaction_builder.h" #include "utiltest.h" #include "zcash/Address.hpp" +#include "zcash/address/mnemonic.h" + +#include #include #include @@ -34,6 +37,10 @@ public: return false; } + bool GetOrchardAnchorAt(const uint256 &rt, OrchardMerkleFrontier &tree) const { + return false; + } + bool GetNullifier(const uint256 &nf, ShieldedType type) const { return false; } @@ -92,7 +99,7 @@ TEST(TransactionBuilder, TransparentToSapling) // Create a shielding transaction from transparent to Sapling // 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000); builder.AddSaplingOutput(fvk_from.ovk, pk, 40000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -124,7 +131,7 @@ TEST(TransactionBuilder, SaplingToSapling) { // Create a Sapling-only transaction // 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00005 z-ZEC change - auto builder = TransactionBuilder(consensusParams, 2); + auto builder = TransactionBuilder(consensusParams, 2, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); // Check that trying to add a different anchor fails @@ -165,7 +172,7 @@ TEST(TransactionBuilder, SaplingToSprout) { // - 0.0004 Sapling-ZEC in - 0.00025 Sprout-ZEC out // - 0.00005 Sapling-ZEC change // - default t-ZEC fee - auto builder = TransactionBuilder(consensusParams, 2, nullptr); + auto builder = TransactionBuilder(consensusParams, 2, std::nullopt, nullptr); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSproutOutput(sproutAddr, 25000); auto tx = builder.Build().GetTxOrThrow(); @@ -217,7 +224,7 @@ TEST(TransactionBuilder, SproutToSproutAndSapling) { // - 0.00005 Sprout-ZEC change // - 0.00005 Sapling-ZEC out // - 0.00005 t-ZEC fee - auto builder = TransactionBuilder(consensusParams, 2, nullptr, &view); + auto builder = TransactionBuilder(consensusParams, 2, std::nullopt, nullptr, &view); builder.SetFee(5000); builder.AddSproutInput(sproutSk, sproutNote, sproutWitness); builder.AddSproutOutput(sproutAddr, 6000); @@ -248,12 +255,60 @@ TEST(TransactionBuilder, SproutToSproutAndSapling) { RegtestDeactivateSapling(); } +TEST(TransactionBuilder, TransparentToOrchard) +{ + auto consensusParams = RegtestActivateNU5(); + + CBasicKeyStore keystore; + CKey tsk = AddTestCKeyToKeyStore(keystore); + auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); + + auto coinType = Params().BIP44CoinType(); + auto seed = MnemonicSeed::Random(coinType); + auto sk = libzcash::OrchardSpendingKey::ForAccount(seed, coinType, 0); + auto fvk = sk.ToFullViewingKey(); + auto ivk = fvk.ToIncomingViewingKey(); + libzcash::diversifier_index_t j(0); + auto recipient = ivk.Address(j); + + TransactionBuilderCoinsViewDB fakeDB; + auto orchardAnchor = fakeDB.GetBestAnchor(ShieldedType::ORCHARD); + + // Create a shielding transaction from transparent to Orchard + // 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee + auto builder = TransactionBuilder(consensusParams, 1, orchardAnchor, &keystore); + builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000); + builder.AddOrchardOutput(std::nullopt, recipient, 40000); + auto maybeTx = builder.Build(); + EXPECT_TRUE(maybeTx.IsTx()); + if (maybeTx.IsError()) { + std::cerr << "Failed to build transaction: " << maybeTx.GetError() << std::endl; + GTEST_FAIL(); + } + auto tx = maybeTx.GetTxOrThrow(); + + 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(), 0); + EXPECT_TRUE(tx.GetOrchardBundle().IsPresent()); + EXPECT_EQ(tx.GetOrchardBundle().GetValueBalance(), -40000); + + CValidationState state; + EXPECT_TRUE(ContextualCheckTransaction(tx, state, Params(), 2, true)); + EXPECT_EQ(state.GetRejectReason(), ""); + + // Revert to default + RegtestDeactivateNU5(); +} + TEST(TransactionBuilder, ThrowsOnTransparentInputWithoutKeyStore) { SelectParams(CBaseChainParams::REGTEST); const Consensus::Params& consensusParams = Params().GetConsensus(); - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); ASSERT_THROW(builder.AddTransparentInput(COutPoint(), CScript(), 1), std::runtime_error); } @@ -264,7 +319,7 @@ TEST(TransactionBuilder, RejectsInvalidTransparentOutput) // Default CTxDestination type is an invalid address CTxDestination taddr; - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); ASSERT_THROW(builder.AddTransparentOutput(taddr, 50), UniValue); } @@ -289,13 +344,13 @@ TEST(TransactionBuilder, FailsWithNegativeChange) // Fail if there is only a Sapling output // 0.0005 z-ZEC out, default fee - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingOutput(fvk.ovk, pa, 50000, {}); EXPECT_EQ("Change cannot be negative", builder.Build().GetError()); // Fail if there is only a transparent output // 0.0005 t-ZEC out, default fee - builder = TransactionBuilder(consensusParams, 1, &keystore); + builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentOutput(taddr, 50000); EXPECT_EQ("Change cannot be negative", builder.Build().GetError()); @@ -336,14 +391,14 @@ TEST(TransactionBuilder, ChangeOutput) // No change address and no Sapling spends { - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); EXPECT_EQ("Could not determine change address", builder.Build().GetError()); } // Change to the same address as the first Sapling spend { - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); auto tx = builder.Build().GetTxOrThrow(); @@ -358,7 +413,7 @@ TEST(TransactionBuilder, ChangeOutput) // Change to a Sapling address { - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); builder.SendChangeTo(zChangeAddr, fvkOut.ovk); auto tx = builder.Build().GetTxOrThrow(); @@ -373,7 +428,7 @@ TEST(TransactionBuilder, ChangeOutput) // Change to a transparent address { - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000); builder.SendChangeTo(tkeyid, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -405,7 +460,7 @@ TEST(TransactionBuilder, SetFee) // Default fee { - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(fvk.ovk, pa, 25000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -420,7 +475,7 @@ TEST(TransactionBuilder, SetFee) // Configured fee { - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(fvk.ovk, pa, 25000, {}); builder.SetFee(20000); @@ -449,7 +504,7 @@ TEST(TransactionBuilder, CheckSaplingTxVersion) auto pk = sk.default_address(); // Cannot add Sapling outputs to a non-Sapling transaction - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); try { builder.AddSaplingOutput(uint256(), pk, 12345, {}); } catch (std::runtime_error const & err) { diff --git a/src/gtest/test_validation.cpp b/src/gtest/test_validation.cpp index 21833fb63..4f028e6f7 100644 --- a/src/gtest/test_validation.cpp +++ b/src/gtest/test_validation.cpp @@ -172,7 +172,7 @@ TEST(Validation, ContextualCheckInputsDetectsOldBranchId) { // Create a transparent transaction that spends the coin, targeting // a height during the Overwinter epoch. - auto builder = TransactionBuilder(consensusParams, 15, &keystore); + auto builder = TransactionBuilder(consensusParams, 15, std::nullopt, &keystore); builder.AddTransparentInput(utxo, scriptPubKey, coinValue); builder.AddTransparentOutput(destination, 40000); auto tx = builder.Build().GetTxOrThrow(); diff --git a/src/primitives/orchard.h b/src/primitives/orchard.h index c3cf7bebc..71582f3f0 100644 --- a/src/primitives/orchard.h +++ b/src/primitives/orchard.h @@ -11,9 +11,10 @@ #include class OrchardMerkleFrontier; +namespace orchard { class UnauthorizedBundle; } /** - * The Orchard component of a transaction. + * The Orchard component of an authorized transaction. */ class OrchardBundle { @@ -22,7 +23,10 @@ private: /// Memory is allocated by Rust. std::unique_ptr inner; + OrchardBundle(OrchardBundlePtr* bundle) : inner(bundle, orchard_bundle_free) {} + friend class OrchardMerkleFrontier; + friend class orchard::UnauthorizedBundle; public: OrchardBundle() : inner(nullptr, orchard_bundle_free) {} diff --git a/src/rust/include/rust/builder.h b/src/rust/include/rust/builder.h new file mode 100644 index 000000000..796256650 --- /dev/null +++ b/src/rust/include/rust/builder.h @@ -0,0 +1,80 @@ +// Copyright (c) 2022 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +#ifndef ZCASH_RUST_INCLUDE_RUST_BUILDER_H +#define ZCASH_RUST_INCLUDE_RUST_BUILDER_H + +#include "rust/orchard.h" +#include "rust/orchard/keys.h" +#include "rust/transaction.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/// Pointer to Rust-allocated Orchard bundle builder. +struct OrchardBuilderPtr; +typedef struct OrchardBuilderPtr OrchardBuilderPtr; + +/// Pointer to Rust-allocated Orchard bundle without proofs +/// or authorizing data. +struct OrchardUnauthorizedBundlePtr; +typedef struct OrchardUnauthorizedBundlePtr OrchardUnauthorizedBundlePtr; + +/// Construct a new Orchard transaction builder. +OrchardBuilderPtr* orchard_builder_new( + bool spends_enabled, + bool outputs_enabled, + const unsigned char* anchor); + +/// Frees an Orchard builder returned from `orchard_builder_new`. +void orchard_builder_free(OrchardBuilderPtr* ptr); + +/// Adds an address which will receive funds in this bundle. +/// +/// `ovk` is a pointer to the outgoing viewing key to make this recipient recoverable by, +/// or `null` to make the recipient unrecoverable by the sender. +bool orchard_builder_add_recipient( + OrchardBuilderPtr* ptr, + const unsigned char* ovk, + const OrchardRawAddressPtr* recipient, + uint64_t value, + const unsigned char* memo); + +/// Builds a bundle containing the given spent notes and recipients. +/// +/// Returns `null` if an error occurs. +/// +/// `builder` is always freed by this method. +OrchardUnauthorizedBundlePtr* orchard_builder_build(OrchardBuilderPtr* builder); + +/// Frees an Orchard bundle returned from `orchard_bundle_build`. +void orchard_unauthorized_bundle_free(OrchardUnauthorizedBundlePtr* bundle); + +/// Adds proofs and signatures to the bundle. +/// +/// Returns `null` if an error occurs. +/// +/// `bundle` is always freed by this method. +OrchardBundlePtr* orchard_unauthorized_bundle_prove_and_sign( + OrchardUnauthorizedBundlePtr* bundle, + const unsigned char* sighash); + +/// Calculates a ZIP 244 shielded signature digest for the given under-construction +/// transaction. +/// +/// Returns `false` if any of the parameters are invalid; in this case, `sighash_ret` +/// will be unaltered. +/// +/// `preTx` is always freed by this method. +bool zcash_builder_zip244_shielded_signature_digest( + PrecomputedTxParts* preTx, + const OrchardUnauthorizedBundlePtr* bundle, + unsigned char* sighash_ret); + +#ifdef __cplusplus +} +#endif + +#endif // ZCASH_RUST_INCLUDE_RUST_BUILDER_H diff --git a/src/rust/include/rust/orchard.h b/src/rust/include/rust/orchard.h index 106801e08..5cb32ac0f 100644 --- a/src/rust/include/rust/orchard.h +++ b/src/rust/include/rust/orchard.h @@ -13,6 +13,7 @@ extern "C" { #endif +/// Typesafe pointer to a Rust-allocated orchard::bundle::Bundle value struct OrchardBundlePtr; typedef struct OrchardBundlePtr OrchardBundlePtr; diff --git a/src/rust/src/builder_ffi.rs b/src/rust/src/builder_ffi.rs new file mode 100644 index 000000000..30cdd387d --- /dev/null +++ b/src/rust/src/builder_ffi.rs @@ -0,0 +1,169 @@ +use std::convert::TryInto; +use std::ptr; + +use orchard::{ + builder::{Builder, InProgress, Unauthorized, Unproven}, + bundle::{Authorized, Flags}, + keys::OutgoingViewingKey, + value::NoteValue, + Bundle, +}; +use rand_core::OsRng; +use tracing::error; +use zcash_primitives::transaction::{ + components::{sapling, transparent, Amount}, + sighash::{SignableInput, SIGHASH_ALL}, + sighash_v5::v5_signature_hash, + Authorization, TransactionData, TxVersion, +}; + +use crate::{transaction_ffi::PrecomputedTxParts, ORCHARD_PK}; + +#[no_mangle] +pub extern "C" fn orchard_builder_new( + spends_enabled: bool, + outputs_enabled: bool, + anchor: *const [u8; 32], +) -> *mut Builder { + let anchor = unsafe { anchor.as_ref() }.expect("Anchor pointer may not be null."); + Box::into_raw(Box::new(Builder::new( + Flags::from_parts(spends_enabled, outputs_enabled), + orchard::Anchor::from_bytes(*anchor).unwrap(), + ))) +} + +#[no_mangle] +pub extern "C" fn orchard_builder_add_recipient( + builder: *mut Builder, + ovk: *const [u8; 32], + recipient: *const orchard::Address, + value: u64, + memo: *const [u8; 512], +) -> bool { + let builder = unsafe { builder.as_mut() }.expect("Builder may not be null."); + let ovk = unsafe { ovk.as_ref() } + .copied() + .map(OutgoingViewingKey::from); + let recipient = unsafe { recipient.as_ref() }.expect("Recipient may not be null."); + let value = NoteValue::from_raw(value); + let memo = unsafe { memo.as_ref() }.expect("Memo may not be null."); + + match builder.add_recipient(ovk, *recipient, value, Some(*memo)) { + Ok(()) => true, + Err(e) => { + error!("Failed to add Orchard recipient: {}", e); + false + } + } +} + +#[no_mangle] +pub extern "C" fn orchard_builder_free(builder: *mut Builder) { + if !builder.is_null() { + drop(unsafe { Box::from_raw(builder) }); + } +} + +#[no_mangle] +pub extern "C" fn orchard_builder_build( + builder: *mut Builder, +) -> *mut Bundle, Amount> { + if builder.is_null() { + error!("Called with null builder"); + return ptr::null_mut(); + } + let builder = unsafe { Box::from_raw(builder) }; + + match builder.build(OsRng) { + Ok(bundle) => Box::into_raw(Box::new(bundle)), + Err(e) => { + error!("Failed to build Orchard bundle: {:?}", e); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub extern "C" fn orchard_unauthorized_bundle_free( + bundle: *mut Bundle, Amount>, +) { + if !bundle.is_null() { + drop(unsafe { Box::from_raw(bundle) }); + } +} + +#[no_mangle] +pub extern "C" fn orchard_unauthorized_bundle_prove_and_sign( + bundle: *mut Bundle, Amount>, + sighash: *const [u8; 32], +) -> *mut Bundle { + let bundle = unsafe { Box::from_raw(bundle) }; + let sighash = unsafe { sighash.as_ref() }.expect("sighash pointer may not be null."); + let pk = unsafe { ORCHARD_PK.as_ref() }.unwrap(); + + let res = bundle + .create_proof(pk) + .and_then(|b| b.apply_signatures(OsRng, *sighash, &[])); + + match res { + Ok(signed) => Box::into_raw(Box::new(signed)), + Err(e) => { + error!( + "An error occurred while authorizing the orchard bundle: {:?}", + e + ); + std::ptr::null_mut() + } + } +} + +/// Calculates a ZIP 244 shielded signature digest for the given under-construction +/// transaction. +/// +/// Returns `false` if any of the parameters are invalid; in this case, `sighash_ret` +/// will be unaltered. +#[no_mangle] +pub extern "C" fn zcash_builder_zip244_shielded_signature_digest( + precomputed_tx: *mut PrecomputedTxParts, + bundle: *const Bundle, Amount>, + sighash_ret: *mut [u8; 32], +) -> bool { + let precomputed_tx = if !precomputed_tx.is_null() { + unsafe { Box::from_raw(precomputed_tx) } + } else { + error!("Invalid precomputed transaction"); + return false; + }; + if matches!( + precomputed_tx.tx.version(), + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling, + ) { + error!("Cannot calculate ZIP 244 digest for pre-v5 transaction"); + return false; + } + let bundle = unsafe { bundle.as_ref().unwrap() }; + + struct Signable {} + impl Authorization for Signable { + type TransparentAuth = transparent::Authorized; + type SaplingAuth = sapling::Authorized; + type OrchardAuth = InProgress; + } + + let txdata: TransactionData = + precomputed_tx + .tx + .into_data() + .map_bundles(|b| b, |b| b, |_| Some(bundle.clone())); + + let sighash = v5_signature_hash( + &txdata, + SIGHASH_ALL, + &SignableInput::Shielded, + &precomputed_tx.txid_parts, + ); + + // `v5_signature_hash` output is always 32 bytes. + *unsafe { &mut *sighash_ret } = sighash.as_ref().try_into().unwrap(); + true +} diff --git a/src/rust/src/rustzcash.rs b/src/rust/src/rustzcash.rs index 133bbcd53..2309f6339 100644 --- a/src/rust/src/rustzcash.rs +++ b/src/rust/src/rustzcash.rs @@ -71,6 +71,7 @@ mod streams_ffi; mod tracing_ffi; mod address_ffi; +mod builder_ffi; mod history_ffi; mod incremental_merkle_tree_ffi; mod orchard_ffi; diff --git a/src/rust/src/transaction_ffi.rs b/src/rust/src/transaction_ffi.rs index e2515671f..3169ce536 100644 --- a/src/rust/src/transaction_ffi.rs +++ b/src/rust/src/transaction_ffi.rs @@ -56,8 +56,8 @@ pub extern "C" fn zcash_transaction_digests( } pub struct PrecomputedTxParts { - tx: Transaction, - txid_parts: TxDigests, + pub(crate) tx: Transaction, + pub(crate) txid_parts: TxDigests, } /// Precomputes the `TxDigest` struct for the given transaction. diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index 94705ebed..ad9455591 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -15,6 +15,81 @@ #include #include +uint256 ProduceZip244SignatureHash( + const CTransaction& tx, + const orchard::UnauthorizedBundle& orchardBundle) +{ + uint256 dataToBeSigned; + PrecomputedTransactionData local(tx); + if (!zcash_builder_zip244_shielded_signature_digest( + local.preTx.release(), + orchardBundle.inner.get(), + dataToBeSigned.begin())) + { + throw std::logic_error("ZIP 225 signature hash failed"); + } + return dataToBeSigned; +} + +namespace orchard { + +Builder::Builder( + bool spendsEnabled, + bool outputsEnabled, + uint256 anchor) : inner(nullptr, orchard_builder_free) +{ + inner.reset(orchard_builder_new(spendsEnabled, outputsEnabled, anchor.begin())); +} + +void Builder::AddOutput( + const std::optional& ovk, + const libzcash::OrchardRawAddress& to, + CAmount value, + std::array memo) +{ + if (!inner) { + throw std::logic_error("orchard::Builder has already been used"); + } + + orchard_builder_add_recipient( + inner.get(), + ovk.has_value() ? ovk->begin() : nullptr, + to.inner.get(), + value, + memo.data()); +} + +std::optional Builder::Build() { + if (!inner) { + throw std::logic_error("orchard::Builder has already been used"); + } + + auto bundle = orchard_builder_build(inner.release()); + if (bundle == nullptr) { + return std::nullopt; + } else { + return UnauthorizedBundle(bundle); + } +} + +std::optional UnauthorizedBundle::ProveAndSign( + uint256 sighash) +{ + if (!inner) { + throw std::logic_error("orchard::UnauthorizedBundle has already been used"); + } + + auto authorizedBundle = orchard_unauthorized_bundle_prove_and_sign( + inner.release(), sighash.begin()); + if (authorizedBundle == nullptr) { + return std::nullopt; + } else { + return OrchardBundle(authorizedBundle); + } +} + +} // namespace orchard + SpendDescriptionInfo::SpendDescriptionInfo( libzcash::SaplingExpandedSpendingKey expsk, libzcash::SaplingNote note, @@ -150,16 +225,19 @@ std::string TransactionBuilderResult::GetError() { TransactionBuilder::TransactionBuilder( const Consensus::Params& consensusParams, int nHeight, + std::optional orchardAnchor, CKeyStore* keystore, CCoinsViewCache* coinsView, CCriticalSection* cs_coinsView) : - usingSprout(std::nullopt), consensusParams(consensusParams), nHeight(nHeight), keystore(keystore), coinsView(coinsView), cs_coinsView(cs_coinsView) { + if (orchardAnchor.has_value()) { + orchardBuilder = orchard::Builder(true, true, orchardAnchor.value()); + } mtx = CreateNewContextualCMutableTransaction(consensusParams, nHeight); } @@ -174,7 +252,6 @@ private: std::string msg; }; - void TransactionBuilder::SetExpiryHeight(uint32_t nExpiryHeight) { if (nExpiryHeight < nHeight || nExpiryHeight <= 0 || nExpiryHeight >= TX_EXPIRY_HEIGHT_THRESHOLD) { @@ -183,6 +260,20 @@ void TransactionBuilder::SetExpiryHeight(uint32_t nExpiryHeight) mtx.nExpiryHeight = nExpiryHeight; } +void TransactionBuilder::AddOrchardOutput( + const std::optional& ovk, + const libzcash::OrchardRawAddress& to, + CAmount value, + std::array memo) +{ + if (!orchardBuilder.has_value()) { + throw std::runtime_error("TransactionBuilder cannot add Orchard output without Orchard anchor"); + } + + orchardBuilder.value().AddOutput(ovk, to, value, memo); + valueBalanceOrchard -= value; +} + void TransactionBuilder::AddSaplingSpend( libzcash::SaplingExpandedSpendingKey expsk, libzcash::SaplingNote note, @@ -314,7 +405,7 @@ TransactionBuilderResult TransactionBuilder::Build() // // Valid change - CAmount change = mtx.valueBalanceSapling - fee; + CAmount change = mtx.valueBalanceSapling + valueBalanceOrchard - fee; for (auto jsInput : jsInputs) { change += jsInput.note.value(); } @@ -360,6 +451,20 @@ TransactionBuilderResult TransactionBuilder::Build() } } + // + // Orchard + // + + std::optional orchardBundle; + if (orchardBuilder.has_value()) { + auto bundle = orchardBuilder->Build(); + if (bundle.has_value()) { + orchardBundle = std::move(bundle); + } else { + return TransactionBuilderResult("Failed to build Orchard bundle"); + } + } + // // Sapling spends and outputs // @@ -449,14 +554,28 @@ TransactionBuilderResult TransactionBuilder::Build() // Empty output script. uint256 dataToBeSigned; - CScript scriptCode; try { - dataToBeSigned = SignatureHash(scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId); + if (orchardBundle.has_value()) { + // Orchard is only usable with v5+ transactions. + dataToBeSigned = ProduceZip244SignatureHash(mtx, orchardBundle.value()); + } else { + CScript scriptCode; + dataToBeSigned = SignatureHash(scriptCode, mtx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId); + } } catch (std::logic_error ex) { librustzcash_sapling_proving_ctx_free(ctx); return TransactionBuilderResult("Could not construct signature hash: " + std::string(ex.what())); } + if (orchardBundle.has_value()) { + auto authorizedBundle = orchardBundle.value().ProveAndSign(dataToBeSigned); + if (authorizedBundle.has_value()) { + mtx.orchardBundle = authorizedBundle.value(); + } else { + return TransactionBuilderResult("Failed to create Orchard proof or signatures"); + } + } + // Create Sapling spendAuth and binding signatures for (size_t i = 0; i < spends.size(); i++) { librustzcash_sapling_spend_sig( @@ -513,15 +632,11 @@ TransactionBuilderResult TransactionBuilder::Build() void TransactionBuilder::CheckOrSetUsingSprout() { - if (usingSprout.has_value()) { - if (!usingSprout.value()) { - throw JSONRPCError(RPC_WALLET_ERROR, "Can't use Sprout with a v5 transaction."); - } + if (orchardBuilder.has_value()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Can't use Sprout with a v5 transaction."); } else { - usingSprout = true; - // Switch if necessary to a Sprout-supporting transaction format. - auto txVersionInfo = CurrentTxVersionInfo(consensusParams, nHeight, usingSprout.value()); + auto txVersionInfo = CurrentTxVersionInfo(consensusParams, nHeight, true); mtx.nVersionGroupId = txVersionInfo.nVersionGroupId; mtx.nVersion = txVersionInfo.nVersion; mtx.nConsensusBranchId = std::nullopt; diff --git a/src/transaction_builder.h b/src/transaction_builder.h index 4fbdf2c09..db6750d40 100644 --- a/src/transaction_builder.h +++ b/src/transaction_builder.h @@ -21,8 +21,107 @@ #include +#include + #define NO_MEMO {{0xF6}} +namespace orchard { class UnauthorizedBundle; } + +uint256 ProduceZip244SignatureHash( + const CTransaction& tx, + const orchard::UnauthorizedBundle& orchardBundle); + +namespace orchard { + +/// A builder that constructs an `UnauthorizedBundle` from a set of notes to be spent, +/// and recipients to receive funds. +class Builder { +private: + /// The Orchard builder. Memory is allocated by Rust. If this is `nullptr` then + /// `Builder::Build` has been called, and all subsequent operations will throw an + /// exception. + std::unique_ptr inner; + + Builder() : inner(nullptr, orchard_builder_free) { } + +public: + Builder(bool spendsEnabled, bool outputsEnabled, uint256 anchor); + + // Builder should never be copied + Builder(const Builder&) = delete; + Builder& operator=(const Builder&) = delete; + Builder(Builder&& builder) : inner(std::move(builder.inner)) {} + Builder& operator=(Builder&& builder) + { + if (this != &builder) { + inner = std::move(builder.inner); + } + return *this; + } + + /// Adds an address which will receive funds in this bundle. + void AddOutput( + const std::optional& ovk, + const libzcash::OrchardRawAddress& to, + CAmount value, + std::array memo = NO_MEMO); + + /// Builds a bundle containing the given spent notes and recipients. + /// + /// Returns `std::nullopt` if an error occurs. + /// + /// Calling this method invalidates this object; in particular, if an error occurs + /// this builder must be discarded and a new builder created. Subsequent usage of this + /// object in any way will cause an exception. This emulates Rust's compile-time move + /// semantics at runtime. + std::optional Build(); +}; + +/// An unauthorized Orchard bundle, ready for its proof to be created and signatures +/// applied. +class UnauthorizedBundle { +private: + /// An optional Orchard bundle (with `nullptr` corresponding to `None`). + /// Memory is allocated by Rust. + std::unique_ptr inner; + + UnauthorizedBundle() : inner(nullptr, orchard_unauthorized_bundle_free) {} + UnauthorizedBundle(OrchardUnauthorizedBundlePtr* bundle) : inner(bundle, orchard_unauthorized_bundle_free) {} + friend class Builder; + // The parentheses here are necessary to avoid the following compilation error: + // error: C++ requires a type specifier for all declarations + // friend uint256 ::ProduceZip244SignatureHash( + // ~~~~~~ ^ + friend uint256 (::ProduceZip244SignatureHash( + const CTransaction& tx, + const UnauthorizedBundle& orchardBundle)); + +public: + // UnauthorizedBundle should never be copied + UnauthorizedBundle(const UnauthorizedBundle&) = delete; + UnauthorizedBundle& operator=(const UnauthorizedBundle&) = delete; + UnauthorizedBundle(UnauthorizedBundle&& bundle) : inner(std::move(bundle.inner)) {} + UnauthorizedBundle& operator=(UnauthorizedBundle&& bundle) + { + if (this != &bundle) { + inner = std::move(bundle.inner); + } + return *this; + } + + /// Adds proofs and signatures to this bundle. + /// + /// Returns `std::nullopt` if an error occurs. + /// + /// Calling this method invalidates this object; in particular, if an error occurs + /// this bundle must be discarded and a new bundle built. Subsequent usage of this + /// object in any way will cause an exception. This emulates Rust's compile-time + /// move semantics at runtime. + std::optional ProveAndSign(uint256 sighash); +}; + +} // namespace orchard + struct SpendDescriptionInfo { libzcash::SaplingExpandedSpendingKey expsk; libzcash::SaplingNote note; @@ -107,7 +206,6 @@ public: class TransactionBuilder { private: - std::optional usingSprout; Consensus::Params consensusParams; int nHeight; const CKeyStore* keystore; @@ -115,6 +213,8 @@ private: CCriticalSection* cs_coinsView; CMutableTransaction mtx; CAmount fee = 10000; + std::optional orchardBuilder; + CAmount valueBalanceOrchard = 0; std::vector spends; std::vector outputs; @@ -131,14 +231,66 @@ public: TransactionBuilder( const Consensus::Params& consensusParams, int nHeight, + std::optional orchardAnchor, CKeyStore* keyStore = nullptr, CCoinsViewCache* coinsView = nullptr, CCriticalSection* cs_coinsView = nullptr); + // TransactionBuilder should never be copied + TransactionBuilder(const TransactionBuilder&) = delete; + TransactionBuilder& operator=(const TransactionBuilder&) = delete; + TransactionBuilder(TransactionBuilder&& builder) : + consensusParams(std::move(builder.consensusParams)), + nHeight(std::move(builder.nHeight)), + keystore(std::move(builder.keystore)), + coinsView(std::move(builder.coinsView)), + cs_coinsView(std::move(builder.cs_coinsView)), + mtx(std::move(builder.mtx)), + fee(std::move(builder.fee)), + orchardBuilder(std::move(builder.orchardBuilder)), + valueBalanceOrchard(std::move(builder.valueBalanceOrchard)), + spends(std::move(builder.spends)), + outputs(std::move(builder.outputs)), + jsInputs(std::move(builder.jsInputs)), + jsOutputs(std::move(builder.jsOutputs)), + tIns(std::move(builder.tIns)), + saplingChangeAddr(std::move(builder.saplingChangeAddr)), + sproutChangeAddr(std::move(builder.sproutChangeAddr)), + tChangeAddr(std::move(builder.tChangeAddr)) {} + TransactionBuilder& operator=(TransactionBuilder&& builder) + { + if (this != &builder) { + consensusParams = std::move(builder.consensusParams); + nHeight = std::move(builder.nHeight); + keystore = std::move(builder.keystore); + coinsView = std::move(builder.coinsView); + cs_coinsView = std::move(builder.cs_coinsView); + mtx = std::move(builder.mtx); + fee = std::move(builder.fee); + orchardBuilder = std::move(builder.orchardBuilder); + valueBalanceOrchard = std::move(builder.valueBalanceOrchard); + spends = std::move(builder.spends); + outputs = std::move(builder.outputs); + jsInputs = std::move(builder.jsInputs); + jsOutputs = std::move(builder.jsOutputs); + tIns = std::move(builder.tIns); + saplingChangeAddr = std::move(builder.saplingChangeAddr); + sproutChangeAddr = std::move(builder.sproutChangeAddr); + tChangeAddr = std::move(builder.tChangeAddr); + } + return *this; + } + void SetExpiryHeight(uint32_t nExpiryHeight); void SetFee(CAmount fee); + void AddOrchardOutput( + const std::optional& ovk, + const libzcash::OrchardRawAddress& to, + CAmount value, + std::array memo = NO_MEMO); + // Throws if the anchor does not match the anchor used by // previously-added Sapling spends. void AddSaplingSpend( diff --git a/src/utiltest.cpp b/src/utiltest.cpp index 189c7daeb..629645c82 100644 --- a/src/utiltest.cpp +++ b/src/utiltest.cpp @@ -8,6 +8,7 @@ #include "transaction_builder.h" #include +#include #include @@ -348,7 +349,7 @@ CWalletTx GetValidSaplingReceive(const Consensus::Params& consensusParams, auto fvk = sk.expsk.full_viewing_key(); auto pa = sk.ToXFVK().DefaultAddress(); - auto builder = TransactionBuilder(consensusParams, 1, &keyStore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keyStore); builder.SetFee(0); builder.AddTransparentInput(COutPoint(), scriptPubKey, value); builder.AddSaplingOutput(fvk.ovk, pa, value, {}); diff --git a/src/wallet/asyncrpcoperation_mergetoaddress.cpp b/src/wallet/asyncrpcoperation_mergetoaddress.cpp index 405528760..aa440a88e 100644 --- a/src/wallet/asyncrpcoperation_mergetoaddress.cpp +++ b/src/wallet/asyncrpcoperation_mergetoaddress.cpp @@ -89,7 +89,7 @@ AsyncRPCOperation_mergetoaddress::AsyncRPCOperation_mergetoaddress( isUsingBuilder_ = false; if (builder) { isUsingBuilder_ = true; - builder_ = builder.value(); + builder_ = std::move(builder.value()); } KeyIO keyIO(Params()); diff --git a/src/wallet/asyncrpcoperation_saplingmigration.cpp b/src/wallet/asyncrpcoperation_saplingmigration.cpp index 0ae0a76e5..9ba4dca20 100644 --- a/src/wallet/asyncrpcoperation_saplingmigration.cpp +++ b/src/wallet/asyncrpcoperation_saplingmigration.cpp @@ -111,7 +111,7 @@ bool AsyncRPCOperation_saplingmigration::main_impl() { CCoinsViewCache coinsView(pcoinsTip); do { CAmount amountToSend = chooseAmount(availableFunds); - auto builder = TransactionBuilder(consensusParams, targetHeight_, pwalletMain, &coinsView, &cs_main); + auto builder = TransactionBuilder(consensusParams, targetHeight_, std::nullopt, pwalletMain, &coinsView, &cs_main); builder.SetExpiryHeight(targetHeight_ + MIGRATION_EXPIRY_DELTA); LogPrint("zrpcunsafe", "%s: Beginning creating transaction with Sapling output amount=%s\n", getId(), FormatMoney(amountToSend - DEFAULT_FEE)); std::vector fromNotes; diff --git a/src/wallet/asyncrpcoperation_sendmany.cpp b/src/wallet/asyncrpcoperation_sendmany.cpp index a2c21a9fb..3dd252b22 100644 --- a/src/wallet/asyncrpcoperation_sendmany.cpp +++ b/src/wallet/asyncrpcoperation_sendmany.cpp @@ -51,7 +51,7 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany( CAmount fee, bool allowRevealedAmounts, UniValue contextInfo) : - builder_(builder), ztxoSelector_(ztxoSelector), recipients_(recipients), + builder_(std::move(builder)), ztxoSelector_(ztxoSelector), recipients_(recipients), mindepth_(minDepth), fee_(fee), allowRevealedAmounts_(allowRevealedAmounts), contextinfo_(contextInfo) { diff --git a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp index 2fb0fac85..5f2998162 100644 --- a/src/wallet/asyncrpcoperation_shieldcoinbase.cpp +++ b/src/wallet/asyncrpcoperation_shieldcoinbase.cpp @@ -67,7 +67,7 @@ AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase( PaymentAddress toAddress, CAmount fee, UniValue contextInfo) : - builder_(builder), tx_(contextualTx), inputs_(inputs), fee_(fee), contextinfo_(contextInfo) + builder_(std::move(builder)), tx_(contextualTx), inputs_(inputs), fee_(fee), contextinfo_(contextInfo) { assert(contextualTx.nVersion >= 2); // transaction format version must support vJoinSplit diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index bc0016606..137cfa606 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -402,7 +402,7 @@ TEST(WalletTests, SetSaplingNoteAddrsInCWalletTx) { ASSERT_TRUE(nf); uint256 nullifier = nf.value(); - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, note, anchor, witness); builder.AddSaplingOutput(fvk.ovk, pk, 50000, {}); builder.SetFee(0); @@ -537,7 +537,7 @@ TEST(WalletTests, FindMySaplingNotes) { auto testNote = GetTestSaplingNote(pa, 50000); // Generate transaction - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -682,7 +682,7 @@ TEST(WalletTests, GetConflictedSaplingNotes) { auto witness = saplingTree.witness(); // Generate tx to create output note B - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, note, anchor, witness); builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 35000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -738,13 +738,13 @@ TEST(WalletTests, GetConflictedSaplingNotes) { anchor = saplingTree.root(); // Create transaction to spend note B - auto builder2 = TransactionBuilder(consensusParams, 2); + auto builder2 = TransactionBuilder(consensusParams, 2, std::nullopt); builder2.AddSaplingSpend(expsk, note2, anchor, spend_note_witness); builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 20000, {}); auto tx2 = builder2.Build().GetTxOrThrow(); // Create conflicting transaction which also spends note B - auto builder3 = TransactionBuilder(consensusParams, 2); + auto builder3 = TransactionBuilder(consensusParams, 2, std::nullopt); builder3.AddSaplingSpend(expsk, note2, anchor, spend_note_witness); builder3.AddSaplingOutput(extfvk.fvk.ovk, pk, 19999, {}); auto tx3 = builder3.Build().GetTxOrThrow(); @@ -833,7 +833,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) { auto testNote = GetTestSaplingNote(pa, 50000); // Generate transaction - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -918,7 +918,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) { auto testNote = GetTestSaplingNote(pa, 50000); // Generate transaction - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -1054,7 +1054,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) { auto witness = saplingTree.witness(); // Generate transaction, which sends funds to note B - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, note, anchor, witness); builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 25000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -1126,7 +1126,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) { anchor = saplingTree.root(); // Create transaction to spend note B - auto builder2 = TransactionBuilder(consensusParams, 2); + auto builder2 = TransactionBuilder(consensusParams, 2, std::nullopt); builder2.AddSaplingSpend(expsk, note2, anchor, spend_note_witness); builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 12500, {}); auto tx2 = builder2.Build().GetTxOrThrow(); @@ -1861,7 +1861,7 @@ TEST(WalletTests, UpdatedSaplingNoteData) { auto testNote = GetTestSaplingNote(pa, 50000); // Generate transaction - auto builder = TransactionBuilder(consensusParams, 1); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingOutput(extfvk.fvk.ovk, pa2, 25000, {}); auto tx = builder.Build().GetTxOrThrow(); @@ -2004,7 +2004,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) { // Generate shielding tx from transparent to Sapling // 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee - auto builder = TransactionBuilder(consensusParams, 1, &keystore); + auto builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000); builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 40000, {}); auto tx1 = builder.Build().GetTxOrThrow(); @@ -2058,7 +2058,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) { // Create a Sapling-only transaction // 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00005 z-ZEC change - auto builder2 = TransactionBuilder(consensusParams, 2); + auto builder2 = TransactionBuilder(consensusParams, 2, std::nullopt); builder2.AddSaplingSpend(expsk, note, anchor, witness); builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 25000, {}); auto tx2 = builder2.Build().GetTxOrThrow(); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index da4553e9e..d8d038091 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4623,12 +4623,15 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) o.pushKV("fee", std::stod(FormatMoney(nFee))); UniValue contextInfo = o; - TransactionBuilder builder(chainparams.GetConsensus(), nextBlockHeight, pwalletMain); + // TODO: Allow Orchard recipients by specifying an Orchard anchor. + auto orchardAnchor = std::nullopt; + TransactionBuilder builder(chainparams.GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain); // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); std::shared_ptr operation( - new AsyncRPCOperation_sendmany(builder, ztxoSelector, recipients, nMinDepth, nFee, allowRevealedAmounts, contextInfo) + new AsyncRPCOperation_sendmany( + std::move(builder), ztxoSelector, recipients, nMinDepth, nFee, allowRevealedAmounts, contextInfo) ); q->addOperation(operation); AsyncRPCOperationId operationId = operation->getId(); @@ -4972,8 +4975,10 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp) contextInfo.pushKV("fee", ValueFromAmount(nFee)); // Builder (used if Sapling addresses are involved) + // TODO: Allow Orchard recipients by specifying an Orchard anchor. + auto orchardAnchor = std::nullopt; TransactionBuilder builder = TransactionBuilder( - Params().GetConsensus(), nextBlockHeight, pwalletMain); + Params().GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain); // Contextual transaction we will build on // (used if no Sapling addresses are involved) @@ -4985,7 +4990,8 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp) // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); - std::shared_ptr operation( new AsyncRPCOperation_shieldcoinbase(builder, contextualTx, inputs, destaddress.value(), nFee, contextInfo) ); + std::shared_ptr operation( new AsyncRPCOperation_shieldcoinbase( + std::move(builder), contextualTx, inputs, destaddress.value(), nFee, contextInfo) ); q->addOperation(operation); AsyncRPCOperationId operationId = operation->getId(); @@ -5439,12 +5445,15 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) // Builder (used if Sapling addresses are involved) std::optional builder; if (isToSaplingZaddr || saplingNoteInputs.size() > 0) { - builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, pwalletMain); + // TODO: Allow Orchard recipients by specifying an Orchard anchor. + auto orchardAnchor = std::nullopt; + builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain); } // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); std::shared_ptr operation( - new AsyncRPCOperation_mergetoaddress(builder, contextualTx, utxoInputs, sproutNoteInputs, saplingNoteInputs, recipient, nFee, contextInfo) ); + new AsyncRPCOperation_mergetoaddress( + std::move(builder), contextualTx, utxoInputs, sproutNoteInputs, saplingNoteInputs, recipient, nFee, contextInfo) ); q->addOperation(operation); AsyncRPCOperationId operationId = operation->getId(); diff --git a/src/wallet/test/rpc_wallet_tests.cpp b/src/wallet/test/rpc_wallet_tests.cpp index 5b862f584..f62d8eb11 100644 --- a/src/wallet/test/rpc_wallet_tests.cpp +++ b/src/wallet/test/rpc_wallet_tests.cpp @@ -1191,7 +1191,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters) // Mutable tx containing contextual information we need to build tx UniValue retValue = CallRPC("getblockcount"); int nHeight = retValue.get_int(); - TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, pwalletMain); + TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, std::nullopt, pwalletMain); } BOOST_AUTO_TEST_CASE(asyncrpcoperation_sign_send_raw_transaction) { @@ -1234,9 +1234,9 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) // there are no utxos to spend { auto selector = pwalletMain->ZTXOSelectorForAddress(taddr1, true).value(); - TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); + TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain); std::vector recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 1)); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1)); operation->main(); BOOST_CHECK(operation->isFailed()); std::string msg = operation->getErrorMessage(); @@ -1246,9 +1246,9 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) // there are no unspent notes to spend { auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true).value(); - TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); + TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain); std::vector recipients = { SendManyRecipient(taddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 1)); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1)); operation->main(); BOOST_CHECK(operation->isFailed()); std::string msg = operation->getErrorMessage(); @@ -1258,9 +1258,9 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) // get_memo_from_hex_string()) { auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true).value(); - TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); + TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain); std::vector recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 1)); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1)); std::shared_ptr ptr = std::dynamic_pointer_cast (operation); TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr); @@ -1355,12 +1355,12 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling) pwalletMain->AddToWallet(wtx, true, NULL); // Context that z_sendmany requires - auto builder = TransactionBuilder(consensusParams, nextBlockHeight, pwalletMain); + auto builder = TransactionBuilder(consensusParams, nextBlockHeight, std::nullopt, pwalletMain); mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight); auto selector = pwalletMain->ZTXOSelectorForAddress(taddr, true).value(); std::vector recipients = { SendManyRecipient(pa, 1*COIN, "ABCD") }; - std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, selector, recipients, 0)); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 0)); std::shared_ptr ptr = std::dynamic_pointer_cast (operation); // Enable test mode so tx is not sent diff --git a/src/zcash/address/orchard.hpp b/src/zcash/address/orchard.hpp index 60e668f5c..90597570b 100644 --- a/src/zcash/address/orchard.hpp +++ b/src/zcash/address/orchard.hpp @@ -9,6 +9,8 @@ #include "zcash/address/zip32.h" #include +namespace orchard { class Builder; } + namespace libzcash { class OrchardIncomingViewingKey; @@ -23,6 +25,7 @@ private: OrchardRawAddress(OrchardRawAddressPtr* ptr) : inner(ptr, orchard_address_free) {} friend class OrchardIncomingViewingKey; + friend class ::orchard::Builder; public: OrchardRawAddress(OrchardRawAddress&& key) : inner(std::move(key.inner)) {}