diff --git a/src/Bitcoin/DustCalculator.cpp b/src/Bitcoin/DustCalculator.cpp index 3aeea03cecb..01254c12639 100644 --- a/src/Bitcoin/DustCalculator.cpp +++ b/src/Bitcoin/DustCalculator.cpp @@ -15,10 +15,10 @@ Amount FixedDustCalculator::dustAmount([[maybe_unused]] Amount byteFee) noexcept } LegacyDustCalculator::LegacyDustCalculator(TWCoinType coinType) noexcept - : feeCalculator(getFeeCalculator(coinType, false)) { + : feeCalculator(getFeeCalculator(coinType)) { } -Amount LegacyDustCalculator::dustAmount([[maybe_unused]] Amount byteFee) noexcept { +Amount LegacyDustCalculator::dustAmount(Amount byteFee) noexcept { return feeCalculator.calculateSingleInput(byteFee); } diff --git a/src/Bitcoin/FeeCalculator.cpp b/src/Bitcoin/FeeCalculator.cpp index 3294bfb64d5..1b913ec8032 100644 --- a/src/Bitcoin/FeeCalculator.cpp +++ b/src/Bitcoin/FeeCalculator.cpp @@ -4,6 +4,7 @@ #include "FeeCalculator.h" +#include #include using namespace TW; @@ -49,8 +50,9 @@ static constexpr DecredFeeCalculator decredFeeCalculator{}; static constexpr DecredFeeCalculator decredFeeCalculatorNoDustFilter(true); static constexpr SegwitFeeCalculator segwitFeeCalculator{}; static constexpr SegwitFeeCalculator segwitFeeCalculatorNoDustFilter(true); +static constexpr Zip0317FeeCalculator zip0317FeeCalculator{}; -const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter) noexcept { +const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter, bool zip0317) noexcept { switch (coinType) { case TWCoinTypeDecred: if (disableFilter) { @@ -71,6 +73,14 @@ const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter) n } return segwitFeeCalculator; + case TWCoinTypeZcash: + case TWCoinTypeKomodo: + case TWCoinTypeZelcash: + if (zip0317) { + return zip0317FeeCalculator; + } + return defaultFeeCalculator; + default: if (disableFilter) { return defaultFeeCalculatorNoDustFilter; @@ -79,4 +89,11 @@ const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter) n } } +// https://github.com/Zondax/ledger-zcash-tools/blob/5ecf1c04c69d2454b73aa7acea4eadda563dfeff/ledger-zcash-app-builder/src/txbuilder.rs#L342-L363 +int64_t Zip0317FeeCalculator::calculate(int64_t inputs, int64_t outputs, [[maybe_unused]] int64_t byteFee) const noexcept { + const auto logicalActions = std::max(inputs, outputs); + const auto actions = std::max(gGraceActions, logicalActions); + return gMarginalFee * actions; +} + } // namespace TW::Bitcoin diff --git a/src/Bitcoin/FeeCalculator.h b/src/Bitcoin/FeeCalculator.h index 32d0f083959..10c8b9ec691 100644 --- a/src/Bitcoin/FeeCalculator.h +++ b/src/Bitcoin/FeeCalculator.h @@ -88,7 +88,20 @@ class SegwitFeeCalculator : public LinearFeeCalculator { } }; +class Zip0317FeeCalculator: public FeeCalculator { +public: + static constexpr int64_t gMarginalFee = 5000ul; + static constexpr int64_t gGraceActions = 2ul; + + Zip0317FeeCalculator() noexcept = default; + + [[nodiscard]] int64_t calculate(int64_t inputs, int64_t outputs, int64_t byteFee) const noexcept final; + [[nodiscard]] int64_t calculateSingleInput([[maybe_unused]] int64_t byteFee) const noexcept final { + return gMarginalFee; + } +}; + /// Return the fee calculator for the given coin. -const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter = false) noexcept; +const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter = false, bool zip0317 = false) noexcept; } // namespace TW::Bitcoin diff --git a/src/Bitcoin/SigningInput.cpp b/src/Bitcoin/SigningInput.cpp index bd7c86f7378..e7cd22d8417 100644 --- a/src/Bitcoin/SigningInput.cpp +++ b/src/Bitcoin/SigningInput.cpp @@ -38,6 +38,7 @@ SigningInput::SigningInput(const Proto::SigningInput& input) { } lockTime = input.lock_time(); time = input.time(); + zip0317 = input.zip_0317(); extraOutputsAmount = 0; for (auto& output: input.extra_outputs()) { diff --git a/src/Bitcoin/SigningInput.h b/src/Bitcoin/SigningInput.h index 86ced0f1122..d012bfe3d9a 100644 --- a/src/Bitcoin/SigningInput.h +++ b/src/Bitcoin/SigningInput.h @@ -32,6 +32,9 @@ class SigningInput { // Transaction fee per byte Amount byteFee = 0; + // Whether to calculate the fee according to ZIP-0317 for the given transaction + bool zip0317 = false; + // Recipient's address std::string toAddress; diff --git a/src/Bitcoin/TransactionBuilder.cpp b/src/Bitcoin/TransactionBuilder.cpp index beb0a7d682f..068db948fbd 100644 --- a/src/Bitcoin/TransactionBuilder.cpp +++ b/src/Bitcoin/TransactionBuilder.cpp @@ -96,7 +96,7 @@ TransactionPlan TransactionBuilder::plan(const SigningInput& input) { } else if (input.utxos.empty()) { plan.error = Common::Proto::Error_missing_input_utxos; } else { - const auto& feeCalculator = getFeeCalculator(static_cast(input.coinType), input.disableDustFilter); + const auto& feeCalculator = getFeeCalculator(input.coinType, input.disableDustFilter, input.zip0317); auto inputSelector = InputSelector(input.utxos, feeCalculator, input.dustCalculator); auto inputSum = InputSelector::sum(input.utxos); diff --git a/src/Zcash/Transaction.cpp b/src/Zcash/Transaction.cpp index a5432ac2a16..65f19a44e25 100644 --- a/src/Zcash/Transaction.cpp +++ b/src/Zcash/Transaction.cpp @@ -19,10 +19,12 @@ const auto joinsplitsHashPersonalization = Data({'Z', 'c', 'a', 's', 'h', 'J', ' const auto shieldedSpendHashPersonalization = Data({'Z', 'c', 'a', 's', 'h', 'S', 'S', 'p', 'e', 'n', 'd', 's', 'H', 'a', 's', 'h'}); const auto shieldedOutputsHashPersonalization = Data({'Z', 'c', 'a', 's', 'h', 'S', 'O', 'u', 't', 'p', 'u', 't', 'H', 'a', 's', 'h'}); -/// See https://github.com/zcash/zips/blob/master/zip-0205.rst#sapling-deployment BRANCH_ID section +/// See https://github.com/zcash/zips/blob/master/zips/zip-0205.rst#sapling-deployment BRANCH_ID section const std::array SaplingBranchID = {0xbb, 0x09, 0xb8, 0x76}; -/// See https://github.com/zcash/zips/blob/master/zip-0206.rst#blossom-deployment BRANCH_ID section +/// See https://github.com/zcash/zips/blob/master/zips/zip-0206.rst#blossom-deployment BRANCH_ID section const std::array BlossomBranchID = {0x60, 0x0e, 0xb4, 0x2b}; +/// See https://github.com/zcash/zips/blob/main/zips/zip-0253.md#nu6-deployment CONSENSUS_BRANCH_ID section +const std::array Nu6BranchID = {0x55, 0x10, 0xe7, 0xc8}; Data Transaction::getPreImage(const Bitcoin::Script& scriptCode, size_t index, enum TWBitcoinSigHashType hashType, uint64_t amount) const { diff --git a/src/Zcash/Transaction.h b/src/Zcash/Transaction.h index 5c0dd6bd154..7d5fc1534b8 100644 --- a/src/Zcash/Transaction.h +++ b/src/Zcash/Transaction.h @@ -17,6 +17,7 @@ namespace TW::Zcash { extern const std::array SaplingBranchID; extern const std::array BlossomBranchID; +extern const std::array Nu6BranchID; /// Only supports transparent transaction right now /// See also https://github.com/zcash/zips/blob/master/zip-0243.rst diff --git a/src/proto/Bitcoin.proto b/src/proto/Bitcoin.proto index 26fdba4c113..e88c95179a1 100644 --- a/src/proto/Bitcoin.proto +++ b/src/proto/Bitcoin.proto @@ -163,6 +163,10 @@ message SigningInput { // transaction creation time that will be used for verge(xvg) uint32 time = 17; + // Whether to calculate the fee according to ZIP-0317 for the given transaction + // https://zips.z.cash/zip-0317#fee-calculation + bool zip_0317 = 18; + // If set, uses Bitcoin 2.0 Signing protocol. // As a result, `Bitcoin.Proto.SigningOutput.signing_result_v2` is set. BitcoinV2.Proto.SigningInput signing_v2 = 21; diff --git a/tests/chains/Zcash/TWZcashTransactionTests.cpp b/tests/chains/Zcash/TWZcashTransactionTests.cpp index cc20c735e5b..243f9554639 100644 --- a/tests/chains/Zcash/TWZcashTransactionTests.cpp +++ b/tests/chains/Zcash/TWZcashTransactionTests.cpp @@ -205,6 +205,65 @@ TEST(TWZcashTransaction, BlossomSigning) { ASSERT_EQ(hex(serialized), "0400008085202f8901de8c02c79c01018bd91dbc6b293eba03945be25762994409209a06d95c828123000000006b483045022100e6e5071811c08d0c2e81cb8682ee36a8c6b645f5c08747acd3e828de2a4d8a9602200b13b36a838c7e8af81f2d6e7e694ede28833a480cfbaaa68a47187655298a7f0121024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ffffffff01cf440000000000001976a914c3bacb129d85288a3deb5890ca9b711f7f71392688ac00000000000000000000000000000000000000"); } +TEST(TWZcashTransaction, Zip0317Fee) { + // tx on mainnet + // https://blockchair.com/zcash/transaction/092379d65d9b33be1322b2833e20cb573f87e49f73a3537c172354453dcee3a4 + + const auto myAddress = "t1Nx4n8MXhXVTZMY6Vx2zbxsCz5VstD9nuv"; + const auto myPrivateKey = parse_hex("5313c6cb5767fac88a303dab4f5d96ee55b547ec99da0db7a20694ac9e395668"); + + auto input = Bitcoin::Proto::SigningInput(); + input.set_coin_type(TWCoinTypeZcash); + input.set_hash_type(TWBitcoinSigHashTypeAll); + input.set_zip_0317(true); + input.set_to_address("t1S3JTzDWR7FzANsn3erXRPms2BfWVQgH9T"); + input.set_use_max_amount(true); + input.add_private_key(myPrivateKey.data(), myPrivateKey.size()); + + auto txHash = parse_hex("f8a8bdcd4b1b3c6b69b50ebbb26921c43583bb93f20e3ccf3c650791ef969b4e"); + std::reverse(txHash.begin(), txHash.end()); + auto redeemScript = Bitcoin::Script::lockScriptForAddress(myAddress, TWCoinTypeZcash).bytes; + + auto addUtxo = [&txHash, &redeemScript, &input](const uint32_t vout, const int64_t amount) { + auto utxo = input.add_utxo(); + utxo->mutable_out_point()->set_hash(txHash.data(), txHash.size()); + utxo->mutable_out_point()->set_index(vout); + utxo->mutable_out_point()->set_sequence(UINT32_MAX); + utxo->set_script(redeemScript.data(), redeemScript.size()); + utxo->set_amount(amount); + }; + + addUtxo(0, 7000); + addUtxo(1, 1'505'490); + addUtxo(2, 7100); + addUtxo(3, 7200); + addUtxo(4, 7300); + addUtxo(5, 7400); + addUtxo(6, 7500); + addUtxo(7, 7600); + addUtxo(8, 7700); + addUtxo(9, 7800); + addUtxo(10, 7900); + addUtxo(11, 8000); + addUtxo(12, 8001); + addUtxo(13, 8002); + addUtxo(14, 8003); + addUtxo(15, 8004); + + auto plan = Zcash::TransactionBuilder::plan(input); + plan.branchId = Data(Zcash::Nu6BranchID.begin(), Zcash::Nu6BranchID.end()); + *input.mutable_plan() = plan.proto(); + + // Sign + auto result = Bitcoin::TransactionSigner::sign(input); + ASSERT_TRUE(result) << std::to_string(result.error()); + auto signedTx = result.payload(); + + Data serialized; + signedTx.encode(serialized); + ASSERT_EQ(hex(serialized), "0400008085202f89104e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8000000006b4830450221008697d7c738af36b6c2009eee98ab8d10356168cdab1ad3499a993e55ecf5ab56022011762fd1b95abcc55b04a13b395f00d131d2588b29cbb892fa0438920f5bc151012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8010000006b483045022100eb066fc7ab4cbdd42e6e50479bc3e4a5717f0d2c29626831b649d86d8e204df40220333b886a0eb196055f22e19dc9f01c46c57258e91b150cd5587cda1b707a1056012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8020000006b483045022100a1d2744150254ae05942c42721d89e02d0c9992b75d7db2bce3ebfe8e2e6a0e902200abe593108cf1cdddeb02403c15dc087d2dd274c2f85a63bac2248ab2ce3ef34012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8030000006b483045022100f791d7d491a20b7ebd31e0465b9adb83e5994d0fa092c4c213d1a9d97ad2fb3b02207223f97c35cd3f482ff93bdae55a4d7c3087cffd790d689777a9a32271e835c7012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8040000006a47304402204f8fa75701453de79dde52936d2526c6bd31d98d45cbe481df25fcd482054620022056221c611c6af5c66bb302ebadabe76c158aa83c47b4927e90182e6fea0bb392012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8050000006b483045022100d28ed7ea432c2d122815be053c25a044e9d02a8dc5f52e12c58d7a833627a9a90220575fa325028e0abecc2be8c40db5fd8552337dc62d3acb9a8e919dd597927b81012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8060000006b48304502210082dc355620bb855e4fd04984054858376bb28d07f97b149ab49cb7ec6c42559c022005ce1af01f00d452afbc51b8a3c1f14e681f93552e94d66906a71f1ba1c00e3c012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8070000006b483045022100d06a9e04bc6be40913fda047ba19ed24f9a4a8cbd5e338994e22609d6a1a11b202207bf5fee15e9a8c1b17095f7f804d16ba02cba5071bda3383de3ee0a46d3b1dd5012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8080000006a4730440220617f682e60ff8f7fa4784b4d318891cdbac461a99f48087034064ed813d2063f022060cb338a8ee49898ec774d431d0867b5a15382be90c685f39fde4a41af8ff0a7012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8090000006b4830450221008e4f66cb5c69d98cc9a4f1e895fe3c645d4640f4a5f7e8337c3beea34915ab170220320e8d14cd3dbd26eab1c41eca2146089a59dafde04334cd321554183e809417012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80a0000006b483045022100c4bbecaecdf6a9eb4a776b4f99541659dc73b8f2c28937e34e7cb637b5105d8302200092a7ae0eee8b4925e8c207c057f43f705b94e468053d4028a785f4652bc2b1012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80b0000006b4830450221008f2228ac57a30d07cbfed7b0d39977e563d23f4f4776451f76e8b401c618f0710220095a73c8bef932d1865e55656620d3071221be279afd66f0827e39ca4eaa26d3012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80c0000006b483045022100a52c7692a09c308ac9cd87c85afeaa37d69c661b8f7b6cdf8c02876037359cb8022006a3da236a86466add64fa6a38655d2a2b6fba05b84e25fa2583210a435be858012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80d0000006a47304402202ce1f193c23e0262fdf62cb74c1669fa7c9e9de5a801434df43c0dc69b1d6aa1022048641ab533f539a5185136a6b2d933944703fa83ddae233297b98d6f89845792012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80e0000006b483045022100db80a6d02c5cc9c21e94868654be891102a4e664ceab29edbd6ebc9106fc27290220509ddb845a48c2f94f4ec7995d12b01305ecc98eb49dd5b26826f6e4bd1ceaec012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80f0000006b483045022100e40aba96f9dcaafb1ce43acf2cfec44f3c2c59340c8d0fa3cc46c6249efb27ca0220184c20c35ffd585efbb9d36049bcf60670f0120968c435dde2333631b5e1b102012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff01a07f1700000000001976a914599686197c40d39a8e6272355f206a9523fab00288ac00000000000000000000000000000000000000"); +} + TEST(TWZcashTransaction, SigningWithError) { const int64_t amount = 17615; const std::string toAddress = "t1biXYN8wJahR76SqZTe1LBzTLf3JAsmT93";