Skip to content

Commit

Permalink
[ZCash]: Add ZIP-0317 standard used to calculate the ZEC fee correctly (
Browse files Browse the repository at this point in the history
  • Loading branch information
satoshiotomakan authored Jan 7, 2025
1 parent 376076e commit d631d5b
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 7 deletions.
4 changes: 2 additions & 2 deletions src/Bitcoin/DustCalculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
19 changes: 18 additions & 1 deletion src/Bitcoin/FeeCalculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "FeeCalculator.h"

#include <algorithm>
#include <cmath>

using namespace TW;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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
15 changes: 14 additions & 1 deletion src/Bitcoin/FeeCalculator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/Bitcoin/SigningInput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
3 changes: 3 additions & 0 deletions src/Bitcoin/SigningInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/Bitcoin/TransactionBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<TWCoinType>(input.coinType), input.disableDustFilter);
const auto& feeCalculator = getFeeCalculator(input.coinType, input.disableDustFilter, input.zip0317);
auto inputSelector = InputSelector<UTXO>(input.utxos, feeCalculator, input.dustCalculator);
auto inputSum = InputSelector<UTXO>::sum(input.utxos);

Expand Down
6 changes: 4 additions & 2 deletions src/Zcash/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<TW::byte, 4> 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<TW::byte, 4> 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<byte, 4> Nu6BranchID = {0x55, 0x10, 0xe7, 0xc8};

Data Transaction::getPreImage(const Bitcoin::Script& scriptCode, size_t index, enum TWBitcoinSigHashType hashType,
uint64_t amount) const {
Expand Down
1 change: 1 addition & 0 deletions src/Zcash/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace TW::Zcash {

extern const std::array<byte, 4> SaplingBranchID;
extern const std::array<byte, 4> BlossomBranchID;
extern const std::array<byte, 4> Nu6BranchID;

/// Only supports transparent transaction right now
/// See also https://github.com/zcash/zips/blob/master/zip-0243.rst
Expand Down
4 changes: 4 additions & 0 deletions src/proto/Bitcoin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
59 changes: 59 additions & 0 deletions tests/chains/Zcash/TWZcashTransactionTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Zcash::Transaction, Zcash::TransactionBuilder>::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";
Expand Down

0 comments on commit d631d5b

Please sign in to comment.