From d4eee9906080c57b8450a9a02c95e09af45e0f1a Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 14 Mar 2022 16:00:11 +0000 Subject: [PATCH] Add Orchard cases to note selection logic --- src/wallet/gtest/test_note_selection.cpp | 217 ++++++++++++++++++----- src/wallet/orchard.h | 4 + src/wallet/wallet.cpp | 135 ++++++++++++-- src/wallet/wallet.h | 1 + 4 files changed, 298 insertions(+), 59 deletions(-) diff --git a/src/wallet/gtest/test_note_selection.cpp b/src/wallet/gtest/test_note_selection.cpp index 30932b7ed..b4438c283 100644 --- a/src/wallet/gtest/test_note_selection.cpp +++ b/src/wallet/gtest/test_note_selection.cpp @@ -8,6 +8,7 @@ void PrintTo(const OutputPool& pool, std::ostream* os) { switch (pool) { + case OutputPool::Orchard: *os << "Orchard"; break; case OutputPool::Sapling: *os << "Sapling"; break; case OutputPool::Transparent: *os << "Transparent"; break; } @@ -28,6 +29,18 @@ SpendableInputs FakeSpendableInputs( { SpendableInputs inputs; + if (available.count(OutputPool::Orchard)) { + auto seed = MnemonicSeed::Random(0); + auto sk = libzcash::OrchardSpendingKey::ForAccount(seed, 0, 0); + libzcash::diversifier_index_t j(0); + auto address = sk.ToFullViewingKey().ToIncomingViewingKey().Address(j); + for (int i = 0; i < 10; i++) { + OrchardOutPoint op; + inputs.orchardNoteMetadata.push_back(OrchardNoteMetadata{ + op, address, 1, {}}); + } + } + if (available.count(OutputPool::Sapling)) { for (int i = 0; i < 10; i++) { SaplingOutPoint op; @@ -86,15 +99,17 @@ TEST_P(SpendableInputsTest, SelectsSproutBeforeFirst) auto order = std::get<2>(GetParam()); auto wtx = FakeWalletTx(); + bool canSelectSprout = !(recipientPools.count(OutputPool::Orchard)); + // Create a set of inputs from Sprout and the available pools. auto inputs = FakeSpendableInputs(available, true, &wtx); EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1)); // We have Sprout notes along with the expected notes. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); for (auto pool : available) { switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } @@ -104,13 +119,25 @@ TEST_P(SpendableInputsTest, SelectsSproutBeforeFirst) EXPECT_TRUE(inputs.LimitToAmount(5, 1, recipientPools)); EXPECT_EQ(inputs.Total(), 5); - // We only have Sprout notes. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); - EXPECT_EQ(inputs.sproutNoteEntries.size(), 5); - for (auto pool : order[0]) { - switch (pool) { - case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 0); break; - case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 0); break; + if (canSelectSprout) { + // We only have Sprout notes. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 5); + for (auto pool : order[0]) { + switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 0); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 0); break; + } + } + } else { + // We never have Sprout notes. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); + for (auto pool : order[0]) { + switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 5); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 5); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 5); break; + } } } } @@ -122,32 +149,67 @@ TEST_P(SpendableInputsTest, SelectsSproutThenFirst) auto order = std::get<2>(GetParam()); auto wtx = FakeWalletTx(); + bool canSelectSprout = !(recipientPools.count(OutputPool::Orchard)); + // Create a set of inputs from Sprout and the available pools. auto inputs = FakeSpendableInputs(available, true, &wtx); EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1)); // We have Sprout notes along with the expected notes. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); for (auto pool : available) { switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } } - // Limit to 14 zatoshis (which requires two pools). - EXPECT_TRUE(inputs.LimitToAmount(14, 1, std::get<1>(GetParam()))); - EXPECT_EQ(inputs.Total(), 14); + // Limit to 14 zatoshis (which requires two pools). If we only have one pool + // available and can't select Sprout, we won't have sufficient funds. + auto sufficientFunds = inputs.LimitToAmount(14, 1, std::get<1>(GetParam())); + if (available.size() == 1 && !canSelectSprout) { + EXPECT_FALSE(sufficientFunds); + EXPECT_EQ(inputs.Total(), 10); - // We have all Sprout notes and some from the first pool in the first order. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); - EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); - for (int i = 0; i < order[0].size(); i++) { - auto expected = i == 0 ? 4 : 0; - switch (order[0][i]) { - case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; - case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + // We have no Sprout notes and all from the first pool in the first order. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); + for (int i = 0; i < order[0].size(); i++) { + auto expected = i == 0 ? 10 : 0; + switch (order[0][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + } + } + } else { + EXPECT_TRUE(sufficientFunds); + EXPECT_EQ(inputs.Total(), 14); + + if (canSelectSprout) { + // We have all Sprout notes and some from the first pool in the + // first order. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); + for (int i = 0; i < order[0].size(); i++) { + auto expected = i == 0 ? 4 : 0; + switch (order[0][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + } + } + } else { + // We have all notes from the first pool and some from the second, + // in the second order. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); + for (int i = 0; i < order[1].size(); i++) { + auto expected = i == 0 ? 10 : i == 1 ? 4 : 0; + switch (order[1][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + } + } } } } @@ -164,10 +226,10 @@ TEST_P(SpendableInputsTest, SelectsFirstBeforeSecond) EXPECT_EQ(inputs.Total(), 10 * available.size()); // We have the expected notes. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); for (auto pool : available) { switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } @@ -178,11 +240,11 @@ TEST_P(SpendableInputsTest, SelectsFirstBeforeSecond) EXPECT_EQ(inputs.Total(), 8); // We use the first order and only have the first pool selected. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); for (int i = 0; i < order[0].size(); i++) { auto expected = i == 0 ? 8 : 0; switch (order[0][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; } @@ -201,10 +263,10 @@ TEST_P(SpendableInputsTest, SelectsFirstThenSecond) EXPECT_EQ(inputs.Total(), 10 * available.size()); // We have the expected notes. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); for (auto pool : available) { switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } @@ -218,10 +280,10 @@ TEST_P(SpendableInputsTest, SelectsFirstThenSecond) EXPECT_EQ(inputs.Total(), 10); // We have selected all of the available pool. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); for (int i = 0; i < order[0].size(); i++) { switch (order[0][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } @@ -231,11 +293,11 @@ TEST_P(SpendableInputsTest, SelectsFirstThenSecond) EXPECT_EQ(inputs.Total(), 13); // We have all of the first pool and some of the second. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); for (int i = 0; i < order[1].size(); i++) { auto expected = i == 0 ? 10 : i == 1 ? 3 : 0; switch (order[1][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; } @@ -250,32 +312,34 @@ TEST_P(SpendableInputsTest, SelectsSproutAndFirstThenSecond) auto order = std::get<2>(GetParam()); auto wtx = FakeWalletTx(); + bool canSelectSprout = !(recipientPools.count(OutputPool::Orchard)); + // Create a set of inputs from Sprout and the available pools. auto inputs = FakeSpendableInputs(available, true, &wtx); EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1)); // We have Sprout notes along with the expected notes. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); for (auto pool : available) { switch (pool) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } } - // Limit to 24 zatoshis. - // If we only have one pool available, we won't have sufficient funds. + // Limit to 24 zatoshis. If we only have one pool available, or we have two + // pools but can't select Sprout, we won't have sufficient funds. auto sufficientFunds = inputs.LimitToAmount(24, 1, std::get<1>(GetParam())); - if (available.size() == 1) { + if (available.size() == 1 || (available.size() == 2 && !canSelectSprout)) { EXPECT_FALSE(sufficientFunds); - EXPECT_EQ(inputs.Total(), 20); + EXPECT_EQ(inputs.Total(), (canSelectSprout || available.size() == 2) ? 20 : 10); // We have selected all of the available pool. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); - EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); + EXPECT_EQ(inputs.sproutNoteEntries.size(), canSelectSprout ? 10 : 0); for (int i = 0; i < order[0].size(); i++) { switch (order[0][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), 10); break; case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break; case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break; } @@ -284,14 +348,29 @@ TEST_P(SpendableInputsTest, SelectsSproutAndFirstThenSecond) EXPECT_TRUE(sufficientFunds); EXPECT_EQ(inputs.Total(), 24); - // We have all of Sprout and the first pool, and some of the second. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); - EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); - for (int i = 0; i < order[1].size(); i++) { - auto expected = i == 0 ? 10 : i == 1 ? 4 : 0; - switch (order[1][i]) { - case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; - case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + if (canSelectSprout) { + // We have all of Sprout and the first pool, and some of the second, + // in the second order. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 10); + for (int i = 0; i < order[1].size(); i++) { + auto expected = i == 0 ? 10 : i == 1 ? 4 : 0; + switch (order[1][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + } + } + } else { + // We have all notes from the first and second pools and some from + // the third, in the third order. + EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); + for (int i = 0; i < order[2].size(); i++) { + auto expected = i <= 1 ? 10 : i == 2 ? 4 : 0; + switch (order[2][i]) { + case OutputPool::Orchard: EXPECT_EQ(inputs.orchardNoteMetadata.size(), expected); break; + case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break; + case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break; + } } } } @@ -315,10 +394,15 @@ TEST_P(SpendableInputsTest, OpportunisticShielding) EXPECT_EQ(inputs.Total(), 10 * available.size()); // Remove notes from the shielded pools, so we have more transparent funds. - EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0); EXPECT_EQ(inputs.sproutNoteEntries.size(), 0); for (auto pool : available) { switch (pool) { + case OutputPool::Orchard: + while (inputs.orchardNoteMetadata.size() > 3) { + inputs.orchardNoteMetadata.pop_back(); + } + EXPECT_EQ(inputs.orchardNoteMetadata.size(), 3); + break; case OutputPool::Sapling: while (inputs.saplingNoteEntries.size() > 3) { inputs.saplingNoteEntries.pop_back(); @@ -344,12 +428,19 @@ TEST_P(SpendableInputsTest, OpportunisticShielding) const std::set SET_T({OutputPool::Transparent}); const std::set SET_S({OutputPool::Sapling}); +const std::set SET_O({OutputPool::Orchard}); const std::set SET_TS({OutputPool::Transparent, OutputPool::Sapling}); +const std::set SET_TO({OutputPool::Transparent, OutputPool::Orchard}); +const std::set SET_SO({OutputPool::Sapling, OutputPool::Orchard}); +const std::set SET_TSO({OutputPool::Transparent, OutputPool::Sapling, OutputPool::Orchard}); const std::vector VEC_T({OutputPool::Transparent}); const std::vector VEC_S({OutputPool::Sapling}); +const std::vector VEC_O({OutputPool::Orchard}); const std::vector VEC_TS({OutputPool::Transparent, OutputPool::Sapling}); -const std::vector VEC_ST({OutputPool::Sapling, OutputPool::Transparent}); +const std::vector VEC_TO({OutputPool::Transparent, OutputPool::Orchard}); +const std::vector VEC_SO({OutputPool::Sapling, OutputPool::Orchard}); +const std::vector VEC_TSO({OutputPool::Transparent, OutputPool::Sapling, OutputPool::Orchard}); INSTANTIATE_TEST_CASE_P( ExhaustiveCases, @@ -359,12 +450,52 @@ INSTANTIATE_TEST_CASE_P( // ----------|---------------------|-------------------//---------- std::make_tuple(SET_T, SET_T, std::vector({VEC_T})), // N/A std::make_tuple(SET_T, SET_S, std::vector({VEC_T})), // N/A + std::make_tuple(SET_T, SET_O, std::vector({VEC_T})), // N/A std::make_tuple(SET_T, SET_TS, std::vector({VEC_T})), // N/A + std::make_tuple(SET_T, SET_TO, std::vector({VEC_T})), // N/A + std::make_tuple(SET_T, SET_SO, std::vector({VEC_T})), // N/A + std::make_tuple(SET_T, SET_TSO, std::vector({VEC_T})), // N/A std::make_tuple(SET_S, SET_T, std::vector({VEC_S})), // N/A std::make_tuple(SET_S, SET_S, std::vector({VEC_S})), // N/A + std::make_tuple(SET_S, SET_O, std::vector({VEC_S})), // N/A std::make_tuple(SET_S, SET_TS, std::vector({VEC_S})), // N/A + std::make_tuple(SET_S, SET_TO, std::vector({VEC_S})), // N/A + std::make_tuple(SET_S, SET_SO, std::vector({VEC_S})), // N/A + std::make_tuple(SET_S, SET_TSO, std::vector({VEC_S})), // N/A + std::make_tuple(SET_O, SET_T, std::vector({VEC_O})), // N/A + std::make_tuple(SET_O, SET_S, std::vector({VEC_O})), // N/A + std::make_tuple(SET_O, SET_O, std::vector({VEC_O})), // N/A + std::make_tuple(SET_O, SET_TS, std::vector({VEC_O})), // N/A + std::make_tuple(SET_O, SET_TO, std::vector({VEC_O})), // N/A + std::make_tuple(SET_O, SET_SO, std::vector({VEC_O})), // N/A + std::make_tuple(SET_O, SET_TSO, std::vector({VEC_O})), // N/A std::make_tuple(SET_TS, SET_T, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding std::make_tuple(SET_TS, SET_S, std::vector({VEC_S, VEC_TS})), // Fully shielded, opportunistic shielding - std::make_tuple(SET_TS, SET_TS, std::vector({VEC_S, VEC_TS})) // Hide sender, opportunistic shielding + std::make_tuple(SET_TS, SET_O, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TS, SET_TS, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TS, SET_TO, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TS, SET_SO, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TS, SET_TSO, std::vector({VEC_S, VEC_TS})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TO, SET_T, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TO, SET_S, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TO, SET_O, std::vector({VEC_O, VEC_TO})), // Fully shielded, opportunistic shielding + std::make_tuple(SET_TO, SET_TS, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TO, SET_TO, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TO, SET_SO, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding + std::make_tuple(SET_TO, SET_TSO, std::vector({VEC_O, VEC_TO})), // Hide sender, opportunistic shielding + std::make_tuple(SET_SO, SET_T, std::vector({VEC_O, VEC_SO})), // Fewer pools, opportunistic migration + std::make_tuple(SET_SO, SET_S, std::vector({VEC_S, VEC_SO})), // Fully shielded + std::make_tuple(SET_SO, SET_O, std::vector({VEC_O, VEC_SO})), // Fully shielded, opportunistic migration + std::make_tuple(SET_SO, SET_TS, std::vector({VEC_S, VEC_SO})), // Fewer pools + std::make_tuple(SET_SO, SET_TO, std::vector({VEC_O, VEC_SO})), // Fewer pools, opportunistic migration + std::make_tuple(SET_SO, SET_SO, std::vector({VEC_S, VEC_SO})), // Opportunistic migration + std::make_tuple(SET_SO, SET_TSO, std::vector({VEC_S, VEC_SO})), // Opportunistic migration + std::make_tuple(SET_TSO, SET_T, std::vector({VEC_O, VEC_SO, VEC_TSO})), // Fewer pools, hide sender, opportunistic shielding + std::make_tuple(SET_TSO, SET_S, std::vector({VEC_S, VEC_SO, VEC_TSO})), // Fully shielded, hide sender, opportunistic shielding + std::make_tuple(SET_TSO, SET_O, std::vector({VEC_O, VEC_SO, VEC_TSO})), // Fully shielded, hide sender, opportunistic shielding + std::make_tuple(SET_TSO, SET_TS, std::vector({VEC_S, VEC_SO, VEC_TSO})), // Fewer pools, hide sender, opportunistic shielding + std::make_tuple(SET_TSO, SET_TO, std::vector({VEC_O, VEC_SO, VEC_TSO})), // Fewer pools, hide sender, opportunistic shielding + std::make_tuple(SET_TSO, SET_SO, std::vector({VEC_S, VEC_SO, VEC_TSO})), // Opportunistic migration, hide sender, opportunistic shielding + std::make_tuple(SET_TSO, SET_TSO, std::vector({VEC_S, VEC_SO, VEC_TSO})) // Opportunistic migration, hide sender, opportunistic shielding ) ); diff --git a/src/wallet/orchard.h b/src/wallet/orchard.h index 76582f6a4..3ebc5e5cf 100644 --- a/src/wallet/orchard.h +++ b/src/wallet/orchard.h @@ -53,6 +53,10 @@ public: CAmount GetNoteValue() const { return noteValue; } + + const std::array& GetMemo() const { + return memo; + } }; /** diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index ceb644f5a..d8789be75 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -7086,18 +7086,23 @@ bool SpendableInputs::LimitToAmount( return totalWithExtra == amountRequired || totalWithExtra - amountRequired > dustThreshold; }; - // Select Sprout notes for spending first - if possible, we want users to - // spend any notes that they still have in the Sprout pool. - std::sort(sproutNoteEntries.begin(), sproutNoteEntries.end(), - [](SproutNoteEntry i, SproutNoteEntry j) -> bool { - return i.note.value() > j.note.value(); - }); - auto sproutIt = sproutNoteEntries.begin(); - while (sproutIt != sproutNoteEntries.end() && !haveSufficientFunds()) { - totalSelected += sproutIt->note.value(); - ++sproutIt; + if (recipientPools.count(OutputPool::Orchard)) { + // We cannot select Sprout notes with Orchard recipients. + sproutNoteEntries.clear(); + } else { + // Select Sprout notes for spending first - if possible, we want users to + // spend any notes that they still have in the Sprout pool. + std::sort(sproutNoteEntries.begin(), sproutNoteEntries.end(), + [](SproutNoteEntry i, SproutNoteEntry j) -> bool { + return i.note.value() > j.note.value(); + }); + auto sproutIt = sproutNoteEntries.begin(); + while (sproutIt != sproutNoteEntries.end() && !haveSufficientFunds()) { + totalSelected += sproutIt->note.value(); + ++sproutIt; + } + sproutNoteEntries.erase(sproutIt, sproutNoteEntries.end()); } - sproutNoteEntries.erase(sproutIt, sproutNoteEntries.end()); // Check what input pools we have available. CAmount availableTransparent = std::accumulate( @@ -7111,10 +7116,19 @@ bool SpendableInputs::LimitToAmount( [](CAmount acc, const SaplingNoteEntry& entry) { return acc + entry.note.value(); }); + CAmount availableOrchard = std::accumulate( + orchardNoteMetadata.begin(), + orchardNoteMetadata.end(), + CAmount(0), + [](CAmount acc, const OrchardNoteMetadata& entry) { + return acc + entry.GetNoteValue(); + }); assert(availableTransparent >= 0); assert(availableSapling >= 0); + assert(availableOrchard >= 0); bool haveTransparent = availableTransparent > 0; bool haveSapling = availableSapling > 0; + bool haveOrchard = availableOrchard > 0; std::set available; if (haveTransparent) { available.insert(OutputPool::Transparent); @@ -7122,6 +7136,9 @@ bool SpendableInputs::LimitToAmount( if (haveSapling) { available.insert(OutputPool::Sapling); } + if (haveOrchard) { + available.insert(OutputPool::Orchard); + } // Now determine the order in which to select the remaining notes and coins. // We do this in a way that minimizes information leakage while moving funds @@ -7150,23 +7167,77 @@ bool SpendableInputs::LimitToAmount( // used is the first order in the list that can select sufficient funds. // - T: transparent pool // - S: Sapling pool + // - O: Orchard pool // // Available | Recipients | Order | Rationale // ----------|------------|--------|---------- - // T | ** | T | N/A - // S | ** | S | N/A + // T | *** | T | N/A + // S | *** | S | N/A + // O | *** | O | N/A // TS | T | S, TS | Hide sender, opportunistic shielding // TS | S | S, TS | Fully shielded, opportunistic shielding + // TS | O | S, TS | Hide sender, opportunistic shielding // TS | TS | S, TS | Hide sender, opportunistic shielding + // TS | T O | S, TS | Hide sender, opportunistic shielding + // TS | SO | S, TS | Hide sender, opportunistic shielding + // TS | TSO | S, TS | Hide sender, opportunistic shielding + // T O | T | O, TO | Hide sender, opportunistic shielding + // T O | S | O, TO | Hide sender, opportunistic shielding + // T O | O | O, TO | Fully shielded, opportunistic shielding + // T O | TS | O, TO | Hide sender, opportunistic shielding + // T O | T O | O, TO | Hide sender, opportunistic shielding + // T O | SO | O, TO | Hide sender, opportunistic shielding + // T O | TSO | O, TO | Hide sender, opportunistic shielding + // SO | T | O, SO | Fewer pools, opportunistic migration + // SO | S | S, SO | Fully shielded + // SO | O | O, SO | Fully shielded, opportunistic migration + // SO | TS | S, SO | Fewer pools + // SO | T O | O, SO | Fewer pools, opportunistic migration + // SO | SO | S, SO | Opportunistic migration + // SO | TSO | S, SO | Opportunistic migration + // TSO | T | O, SO, TSO | Fewer pools, hide sender, opportunistic shielding + // TSO | S | S, SO, TSO | Fully shielded, hide sender, opportunistic shielding + // TSO | O | O, SO, TSO | Fully shielded, hide sender, opportunistic shielding + // TSO | TS | S, SO, TSO | Fewer pools, hide sender, opportunistic shielding + // TSO | T O | O, SO, TSO | Fewer pools, hide sender, opportunistic shielding + // TSO | SO | S, SO, TSO | Opportunistic migration, hide sender, opportunistic shielding + // TSO | TSO | S, SO, TSO | Opportunistic migration, hide sender, opportunistic shielding std::vector selectionOrder; bool opportunisticShielding = false; if (available.size() <= 1) { // We have at most one input pool, so we don't need selection logic. selectionOrder.assign(available.begin(), available.end()); - } else if (wouldSuffice(availableSapling)) { - // Either fully-shielded, or we hide the sender. + } else if ( + recipientPools == std::set({OutputPool::Orchard}) && + wouldSuffice(availableOrchard)) + { + // Fully shielded. + selectionOrder = { + OutputPool::Orchard, + // Pools below here are erased. + OutputPool::Transparent, + OutputPool::Sapling, + }; + } else if ( + recipientPools.count(OutputPool::Transparent) && + !recipientPools.count(OutputPool::Sapling) && + wouldSuffice(availableOrchard)) + { + // Fewer pools. + selectionOrder = { + OutputPool::Orchard, + // Pools below here are erased. + OutputPool::Transparent, + OutputPool::Sapling, + }; + } else if (wouldSuffice(availableSapling + availableOrchard)) { + // Hide sender. + // This case also handles two other cases: + // - Fully shielded (recipientPools == S && wouldSuffice(S)) + // - Fewer pools (S in recipientPools && O not in recipientPools && wouldSuffice(S)) selectionOrder = { OutputPool::Sapling, + OutputPool::Orchard, // Pools below here are erased. OutputPool::Transparent, }; @@ -7175,13 +7246,20 @@ bool SpendableInputs::LimitToAmount( selectionOrder = { OutputPool::Transparent, OutputPool::Sapling, + OutputPool::Orchard, }; opportunisticShielding = true; } // Ensure we provided a total selection order (so that all unselected notes // and coins are erased). - assert(selectionOrder.size() == available.size()); + for (auto pool : available) { + bool poolIsPresent = false; + for (auto entry : selectionOrder) { + poolIsPresent |= entry == pool; + } + assert(poolIsPresent); + } // Finally, select the remaining notes and coins based on this order. for (auto pool : selectionOrder) { @@ -7221,6 +7299,21 @@ bool SpendableInputs::LimitToAmount( saplingNoteEntries.erase(saplingIt, saplingNoteEntries.end()); break; } + + case OutputPool::Orchard: + { + std::sort(orchardNoteMetadata.begin(), orchardNoteMetadata.end(), + [](OrchardNoteMetadata i, OrchardNoteMetadata j) -> bool { + return i.GetNoteValue() > j.GetNoteValue(); + }); + auto orchardIt = orchardNoteMetadata.begin(); + while (orchardIt != orchardNoteMetadata.end() && !haveSufficientFunds()) { + totalSelected += orchardIt->GetNoteValue(); + ++orchardIt; + } + orchardNoteMetadata.erase(orchardIt, orchardNoteMetadata.end()); + break; + } } } @@ -7269,4 +7362,14 @@ void SpendableInputs::LogInputs(const AsyncRPCOperationId& id) const { FormatMoney(entry.note.value()), HexStr(data).substr(0, 10)); } + + for (const auto& entry : orchardNoteMetadata) { + std::string data(entry.GetMemo().begin(), entry.GetMemo().end()); + LogPrint("zrpcunsafe", "%s: found unspent Orchard note (txid=%s, vActionsOrchard=%d, amount=%s, memo=%s)\n", + id, + entry.GetOutPoint().hash.ToString().substr(0, 10), + entry.GetOutPoint().n, + FormatMoney(entry.GetNoteValue()), + HexStr(data).substr(0, 10)); + } } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index c50a71d80..33088cc46 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -806,6 +806,7 @@ public: * set of output pools the most-preferred pool is selected first. */ enum class OutputPool { + Orchard, Sapling, Transparent, };