Skip to content

Commit d631d5b

Browse files
[ZCash]: Add ZIP-0317 standard used to calculate the ZEC fee correctly (#4198)
1 parent 376076e commit d631d5b

File tree

10 files changed

+107
-7
lines changed

10 files changed

+107
-7
lines changed

src/Bitcoin/DustCalculator.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Amount FixedDustCalculator::dustAmount([[maybe_unused]] Amount byteFee) noexcept
1515
}
1616

1717
LegacyDustCalculator::LegacyDustCalculator(TWCoinType coinType) noexcept
18-
: feeCalculator(getFeeCalculator(coinType, false)) {
18+
: feeCalculator(getFeeCalculator(coinType)) {
1919
}
2020

21-
Amount LegacyDustCalculator::dustAmount([[maybe_unused]] Amount byteFee) noexcept {
21+
Amount LegacyDustCalculator::dustAmount(Amount byteFee) noexcept {
2222
return feeCalculator.calculateSingleInput(byteFee);
2323
}
2424

src/Bitcoin/FeeCalculator.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "FeeCalculator.h"
66

7+
#include <algorithm>
78
#include <cmath>
89

910
using namespace TW;
@@ -49,8 +50,9 @@ static constexpr DecredFeeCalculator decredFeeCalculator{};
4950
static constexpr DecredFeeCalculator decredFeeCalculatorNoDustFilter(true);
5051
static constexpr SegwitFeeCalculator segwitFeeCalculator{};
5152
static constexpr SegwitFeeCalculator segwitFeeCalculatorNoDustFilter(true);
53+
static constexpr Zip0317FeeCalculator zip0317FeeCalculator{};
5254

53-
const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter) noexcept {
55+
const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter, bool zip0317) noexcept {
5456
switch (coinType) {
5557
case TWCoinTypeDecred:
5658
if (disableFilter) {
@@ -71,6 +73,14 @@ const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter) n
7173
}
7274
return segwitFeeCalculator;
7375

76+
case TWCoinTypeZcash:
77+
case TWCoinTypeKomodo:
78+
case TWCoinTypeZelcash:
79+
if (zip0317) {
80+
return zip0317FeeCalculator;
81+
}
82+
return defaultFeeCalculator;
83+
7484
default:
7585
if (disableFilter) {
7686
return defaultFeeCalculatorNoDustFilter;
@@ -79,4 +89,11 @@ const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter) n
7989
}
8090
}
8191

92+
// https://github.com/Zondax/ledger-zcash-tools/blob/5ecf1c04c69d2454b73aa7acea4eadda563dfeff/ledger-zcash-app-builder/src/txbuilder.rs#L342-L363
93+
int64_t Zip0317FeeCalculator::calculate(int64_t inputs, int64_t outputs, [[maybe_unused]] int64_t byteFee) const noexcept {
94+
const auto logicalActions = std::max(inputs, outputs);
95+
const auto actions = std::max(gGraceActions, logicalActions);
96+
return gMarginalFee * actions;
97+
}
98+
8299
} // namespace TW::Bitcoin

src/Bitcoin/FeeCalculator.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,20 @@ class SegwitFeeCalculator : public LinearFeeCalculator {
8888
}
8989
};
9090

91+
class Zip0317FeeCalculator: public FeeCalculator {
92+
public:
93+
static constexpr int64_t gMarginalFee = 5000ul;
94+
static constexpr int64_t gGraceActions = 2ul;
95+
96+
Zip0317FeeCalculator() noexcept = default;
97+
98+
[[nodiscard]] int64_t calculate(int64_t inputs, int64_t outputs, int64_t byteFee) const noexcept final;
99+
[[nodiscard]] int64_t calculateSingleInput([[maybe_unused]] int64_t byteFee) const noexcept final {
100+
return gMarginalFee;
101+
}
102+
};
103+
91104
/// Return the fee calculator for the given coin.
92-
const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter = false) noexcept;
105+
const FeeCalculator& getFeeCalculator(TWCoinType coinType, bool disableFilter = false, bool zip0317 = false) noexcept;
93106

94107
} // namespace TW::Bitcoin

src/Bitcoin/SigningInput.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ SigningInput::SigningInput(const Proto::SigningInput& input) {
3838
}
3939
lockTime = input.lock_time();
4040
time = input.time();
41+
zip0317 = input.zip_0317();
4142

4243
extraOutputsAmount = 0;
4344
for (auto& output: input.extra_outputs()) {

src/Bitcoin/SigningInput.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class SigningInput {
3232
// Transaction fee per byte
3333
Amount byteFee = 0;
3434

35+
// Whether to calculate the fee according to ZIP-0317 for the given transaction
36+
bool zip0317 = false;
37+
3538
// Recipient's address
3639
std::string toAddress;
3740

src/Bitcoin/TransactionBuilder.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ TransactionPlan TransactionBuilder::plan(const SigningInput& input) {
9696
} else if (input.utxos.empty()) {
9797
plan.error = Common::Proto::Error_missing_input_utxos;
9898
} else {
99-
const auto& feeCalculator = getFeeCalculator(static_cast<TWCoinType>(input.coinType), input.disableDustFilter);
99+
const auto& feeCalculator = getFeeCalculator(input.coinType, input.disableDustFilter, input.zip0317);
100100
auto inputSelector = InputSelector<UTXO>(input.utxos, feeCalculator, input.dustCalculator);
101101
auto inputSum = InputSelector<UTXO>::sum(input.utxos);
102102

src/Zcash/Transaction.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ const auto joinsplitsHashPersonalization = Data({'Z', 'c', 'a', 's', 'h', 'J', '
1919
const auto shieldedSpendHashPersonalization = Data({'Z', 'c', 'a', 's', 'h', 'S', 'S', 'p', 'e', 'n', 'd', 's', 'H', 'a', 's', 'h'});
2020
const auto shieldedOutputsHashPersonalization = Data({'Z', 'c', 'a', 's', 'h', 'S', 'O', 'u', 't', 'p', 'u', 't', 'H', 'a', 's', 'h'});
2121

22-
/// See https://github.com/zcash/zips/blob/master/zip-0205.rst#sapling-deployment BRANCH_ID section
22+
/// See https://github.com/zcash/zips/blob/master/zips/zip-0205.rst#sapling-deployment BRANCH_ID section
2323
const std::array<TW::byte, 4> SaplingBranchID = {0xbb, 0x09, 0xb8, 0x76};
24-
/// See https://github.com/zcash/zips/blob/master/zip-0206.rst#blossom-deployment BRANCH_ID section
24+
/// See https://github.com/zcash/zips/blob/master/zips/zip-0206.rst#blossom-deployment BRANCH_ID section
2525
const std::array<TW::byte, 4> BlossomBranchID = {0x60, 0x0e, 0xb4, 0x2b};
26+
/// See https://github.com/zcash/zips/blob/main/zips/zip-0253.md#nu6-deployment CONSENSUS_BRANCH_ID section
27+
const std::array<byte, 4> Nu6BranchID = {0x55, 0x10, 0xe7, 0xc8};
2628

2729
Data Transaction::getPreImage(const Bitcoin::Script& scriptCode, size_t index, enum TWBitcoinSigHashType hashType,
2830
uint64_t amount) const {

src/Zcash/Transaction.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace TW::Zcash {
1717

1818
extern const std::array<byte, 4> SaplingBranchID;
1919
extern const std::array<byte, 4> BlossomBranchID;
20+
extern const std::array<byte, 4> Nu6BranchID;
2021

2122
/// Only supports transparent transaction right now
2223
/// See also https://github.com/zcash/zips/blob/master/zip-0243.rst

src/proto/Bitcoin.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ message SigningInput {
163163
// transaction creation time that will be used for verge(xvg)
164164
uint32 time = 17;
165165

166+
// Whether to calculate the fee according to ZIP-0317 for the given transaction
167+
// https://zips.z.cash/zip-0317#fee-calculation
168+
bool zip_0317 = 18;
169+
166170
// If set, uses Bitcoin 2.0 Signing protocol.
167171
// As a result, `Bitcoin.Proto.SigningOutput.signing_result_v2` is set.
168172
BitcoinV2.Proto.SigningInput signing_v2 = 21;

tests/chains/Zcash/TWZcashTransactionTests.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,65 @@ TEST(TWZcashTransaction, BlossomSigning) {
205205
ASSERT_EQ(hex(serialized), "0400008085202f8901de8c02c79c01018bd91dbc6b293eba03945be25762994409209a06d95c828123000000006b483045022100e6e5071811c08d0c2e81cb8682ee36a8c6b645f5c08747acd3e828de2a4d8a9602200b13b36a838c7e8af81f2d6e7e694ede28833a480cfbaaa68a47187655298a7f0121024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382ffffffff01cf440000000000001976a914c3bacb129d85288a3deb5890ca9b711f7f71392688ac00000000000000000000000000000000000000");
206206
}
207207

208+
TEST(TWZcashTransaction, Zip0317Fee) {
209+
// tx on mainnet
210+
// https://blockchair.com/zcash/transaction/092379d65d9b33be1322b2833e20cb573f87e49f73a3537c172354453dcee3a4
211+
212+
const auto myAddress = "t1Nx4n8MXhXVTZMY6Vx2zbxsCz5VstD9nuv";
213+
const auto myPrivateKey = parse_hex("5313c6cb5767fac88a303dab4f5d96ee55b547ec99da0db7a20694ac9e395668");
214+
215+
auto input = Bitcoin::Proto::SigningInput();
216+
input.set_coin_type(TWCoinTypeZcash);
217+
input.set_hash_type(TWBitcoinSigHashTypeAll);
218+
input.set_zip_0317(true);
219+
input.set_to_address("t1S3JTzDWR7FzANsn3erXRPms2BfWVQgH9T");
220+
input.set_use_max_amount(true);
221+
input.add_private_key(myPrivateKey.data(), myPrivateKey.size());
222+
223+
auto txHash = parse_hex("f8a8bdcd4b1b3c6b69b50ebbb26921c43583bb93f20e3ccf3c650791ef969b4e");
224+
std::reverse(txHash.begin(), txHash.end());
225+
auto redeemScript = Bitcoin::Script::lockScriptForAddress(myAddress, TWCoinTypeZcash).bytes;
226+
227+
auto addUtxo = [&txHash, &redeemScript, &input](const uint32_t vout, const int64_t amount) {
228+
auto utxo = input.add_utxo();
229+
utxo->mutable_out_point()->set_hash(txHash.data(), txHash.size());
230+
utxo->mutable_out_point()->set_index(vout);
231+
utxo->mutable_out_point()->set_sequence(UINT32_MAX);
232+
utxo->set_script(redeemScript.data(), redeemScript.size());
233+
utxo->set_amount(amount);
234+
};
235+
236+
addUtxo(0, 7000);
237+
addUtxo(1, 1'505'490);
238+
addUtxo(2, 7100);
239+
addUtxo(3, 7200);
240+
addUtxo(4, 7300);
241+
addUtxo(5, 7400);
242+
addUtxo(6, 7500);
243+
addUtxo(7, 7600);
244+
addUtxo(8, 7700);
245+
addUtxo(9, 7800);
246+
addUtxo(10, 7900);
247+
addUtxo(11, 8000);
248+
addUtxo(12, 8001);
249+
addUtxo(13, 8002);
250+
addUtxo(14, 8003);
251+
addUtxo(15, 8004);
252+
253+
auto plan = Zcash::TransactionBuilder::plan(input);
254+
plan.branchId = Data(Zcash::Nu6BranchID.begin(), Zcash::Nu6BranchID.end());
255+
*input.mutable_plan() = plan.proto();
256+
257+
// Sign
258+
auto result = Bitcoin::TransactionSigner<Zcash::Transaction, Zcash::TransactionBuilder>::sign(input);
259+
ASSERT_TRUE(result) << std::to_string(result.error());
260+
auto signedTx = result.payload();
261+
262+
Data serialized;
263+
signedTx.encode(serialized);
264+
ASSERT_EQ(hex(serialized), "0400008085202f89104e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8000000006b4830450221008697d7c738af36b6c2009eee98ab8d10356168cdab1ad3499a993e55ecf5ab56022011762fd1b95abcc55b04a13b395f00d131d2588b29cbb892fa0438920f5bc151012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8010000006b483045022100eb066fc7ab4cbdd42e6e50479bc3e4a5717f0d2c29626831b649d86d8e204df40220333b886a0eb196055f22e19dc9f01c46c57258e91b150cd5587cda1b707a1056012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8020000006b483045022100a1d2744150254ae05942c42721d89e02d0c9992b75d7db2bce3ebfe8e2e6a0e902200abe593108cf1cdddeb02403c15dc087d2dd274c2f85a63bac2248ab2ce3ef34012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8030000006b483045022100f791d7d491a20b7ebd31e0465b9adb83e5994d0fa092c4c213d1a9d97ad2fb3b02207223f97c35cd3f482ff93bdae55a4d7c3087cffd790d689777a9a32271e835c7012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8040000006a47304402204f8fa75701453de79dde52936d2526c6bd31d98d45cbe481df25fcd482054620022056221c611c6af5c66bb302ebadabe76c158aa83c47b4927e90182e6fea0bb392012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8050000006b483045022100d28ed7ea432c2d122815be053c25a044e9d02a8dc5f52e12c58d7a833627a9a90220575fa325028e0abecc2be8c40db5fd8552337dc62d3acb9a8e919dd597927b81012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8060000006b48304502210082dc355620bb855e4fd04984054858376bb28d07f97b149ab49cb7ec6c42559c022005ce1af01f00d452afbc51b8a3c1f14e681f93552e94d66906a71f1ba1c00e3c012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8070000006b483045022100d06a9e04bc6be40913fda047ba19ed24f9a4a8cbd5e338994e22609d6a1a11b202207bf5fee15e9a8c1b17095f7f804d16ba02cba5071bda3383de3ee0a46d3b1dd5012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8080000006a4730440220617f682e60ff8f7fa4784b4d318891cdbac461a99f48087034064ed813d2063f022060cb338a8ee49898ec774d431d0867b5a15382be90c685f39fde4a41af8ff0a7012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f8090000006b4830450221008e4f66cb5c69d98cc9a4f1e895fe3c645d4640f4a5f7e8337c3beea34915ab170220320e8d14cd3dbd26eab1c41eca2146089a59dafde04334cd321554183e809417012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80a0000006b483045022100c4bbecaecdf6a9eb4a776b4f99541659dc73b8f2c28937e34e7cb637b5105d8302200092a7ae0eee8b4925e8c207c057f43f705b94e468053d4028a785f4652bc2b1012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80b0000006b4830450221008f2228ac57a30d07cbfed7b0d39977e563d23f4f4776451f76e8b401c618f0710220095a73c8bef932d1865e55656620d3071221be279afd66f0827e39ca4eaa26d3012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80c0000006b483045022100a52c7692a09c308ac9cd87c85afeaa37d69c661b8f7b6cdf8c02876037359cb8022006a3da236a86466add64fa6a38655d2a2b6fba05b84e25fa2583210a435be858012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80d0000006a47304402202ce1f193c23e0262fdf62cb74c1669fa7c9e9de5a801434df43c0dc69b1d6aa1022048641ab533f539a5185136a6b2d933944703fa83ddae233297b98d6f89845792012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80e0000006b483045022100db80a6d02c5cc9c21e94868654be891102a4e664ceab29edbd6ebc9106fc27290220509ddb845a48c2f94f4ec7995d12b01305ecc98eb49dd5b26826f6e4bd1ceaec012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff4e9b96ef9107653ccf3c0ef293bb8335c42169b2bb0eb5696b3c1b4bcdbda8f80f0000006b483045022100e40aba96f9dcaafb1ce43acf2cfec44f3c2c59340c8d0fa3cc46c6249efb27ca0220184c20c35ffd585efbb9d36049bcf60670f0120968c435dde2333631b5e1b102012103b6ced6ffee0d78974da26d910c8b36781e8598019a3982a04286384452418405ffffffff01a07f1700000000001976a914599686197c40d39a8e6272355f206a9523fab00288ac00000000000000000000000000000000000000");
265+
}
266+
208267
TEST(TWZcashTransaction, SigningWithError) {
209268
const int64_t amount = 17615;
210269
const std::string toAddress = "t1biXYN8wJahR76SqZTe1LBzTLf3JAsmT93";

0 commit comments

Comments
 (0)