PIVX Core  5.6.99
P2P Digital Currency
script_P2CS_tests.cpp
Go to the documentation of this file.
1 // Copyright (c) 2020-2021 The PIVX Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or https://www.opensource.org/licenses/mit-license.php.
4 #include "key.h"
5 #include "key_io.h"
6 #include "policy/policy.h"
8 #include "wallet/wallet.h"
9 
10 #include <boost/test/unit_test.hpp>
11 
13 
14 void CheckValidKeyId(const CTxDestination& dest, const CKeyID& expectedKey)
15 {
16  const CKeyID* keyid = boost::get<CKeyID>(&dest);
17  if (keyid) {
18  BOOST_CHECK(keyid);
19  BOOST_CHECK(*keyid == expectedKey);
20  } else {
21  BOOST_ERROR("Destination is not a CKeyID");
22  }
23 }
24 
25 // Goal: check cold staking script keys extraction
26 BOOST_AUTO_TEST_CASE(extract_cold_staking_destination_keys)
27 {
28  CKey ownerKey;
29  ownerKey.MakeNewKey(true);
30  CKeyID ownerId = ownerKey.GetPubKey().GetID();
31  CKey stakerKey;
32  stakerKey.MakeNewKey(true);
33  CKeyID stakerId = stakerKey.GetPubKey().GetID();
34  CScript script = GetScriptForStakeDelegation(stakerId, ownerId);
35 
36  // Check owner
37  CTxDestination ownerDest;
38  BOOST_CHECK(ExtractDestination(script, ownerDest, false));
39  CheckValidKeyId(ownerDest, ownerId);
40 
41  // Check staker
42  CTxDestination stakerDest;
43  BOOST_CHECK(ExtractDestination(script, stakerDest, true));
44  CheckValidKeyId(stakerDest, stakerId);
45 
46  // Now go with ExtractDestinations.
47  txnouttype type;
48  int nRequiredRet = -1;
49  std::vector<CTxDestination> destVector;
50  BOOST_CHECK(ExtractDestinations(script, type, destVector, nRequiredRet));
51  BOOST_CHECK(type == TX_COLDSTAKE);
52  BOOST_CHECK(nRequiredRet == 2);
53  BOOST_CHECK(destVector.size() == 2);
54  CheckValidKeyId(destVector[0], stakerId);
55  CheckValidKeyId(destVector[1], ownerId);
56 }
57 
58 static CScript GetNewP2CS(CKey& stakerKey, CKey& ownerKey, bool fLastOutFree)
59 {
60  stakerKey = KeyIO::DecodeSecret("YNdsth3BsW53DYmCiR12SofWSAt2utXQUSGoin3PekVQCMbzfS7E");
61  ownerKey = KeyIO::DecodeSecret("YUo8oW3y8cUQdQxQxCdnUJ4Ww5H7nHBEMwD2bNDpBbuLM59t4rvd");
62  return fLastOutFree ? GetScriptForStakeDelegationLOF(stakerKey.GetPubKey().GetID(), ownerKey.GetPubKey().GetID())
63  : GetScriptForStakeDelegation(stakerKey.GetPubKey().GetID(), ownerKey.GetPubKey().GetID());
64 }
65 
66 static CScript GetDummyP2CS(const CKeyID& dummyKeyID)
67 {
68  return GetScriptForStakeDelegation(dummyKeyID, dummyKeyID);
69 }
70 
71 static CScript GetDummyP2PKH(const CKeyID& dummyKeyID)
72 {
73  return GetScriptForDestination(dummyKeyID);
74 }
75 
76 static const CAmount amtIn = 200 * COIN;
77 static const unsigned int flags = STANDARD_SCRIPT_VERIFY_FLAGS;
78 
79 static CMutableTransaction CreateNewColdStakeTx(CScript& scriptP2CS, CKey& stakerKey, CKey& ownerKey, bool fLastOutFree)
80 {
81  scriptP2CS = GetNewP2CS(stakerKey, ownerKey, fLastOutFree);
82 
83  // Create prev transaction:
84  CMutableTransaction txFrom;
85  txFrom.vout.resize(1);
86  txFrom.vout[0].nValue = amtIn;
87  txFrom.vout[0].scriptPubKey = scriptP2CS;
88 
89  // Create coldstake
91  tx.vin.resize(1);
92  tx.vout.resize(2);
93  tx.vin[0].prevout.n = 0;
94  tx.vin[0].prevout.hash = txFrom.GetHash();
95  tx.vout[0].nValue = 0;
96  tx.vout[0].scriptPubKey.clear();
97  tx.vout[1].nValue = amtIn + 2 * COIN;
98  tx.vout[1].scriptPubKey = scriptP2CS;
99 
100  return tx;
101 }
102 
103 void SignColdStake(CMutableTransaction& tx, int nIn, const CScript& prevScript, const CKey& key, bool fStaker)
104 {
105  assert(nIn < (int) tx.vin.size());
106  tx.vin[nIn].scriptSig.clear();
107  const CTransaction _tx(tx);
109  const uint256& hash = SignatureHash(prevScript, _tx, nIn, SIGHASH_ALL, amtIn, sv);
110  std::vector<unsigned char> vchSig;
111  BOOST_CHECK(key.Sign(hash, vchSig));
112  vchSig.push_back((unsigned char)SIGHASH_ALL);
113  std::vector<unsigned char> selector(1, fStaker ? (int) OP_TRUE : OP_FALSE);
114  tx.vin[nIn].scriptSig << vchSig << selector << ToByteVector(key.GetPubKey());
115 }
116 
117 static bool CheckP2CSScript(const CScript& scriptSig, const CScript& scriptPubKey, const CMutableTransaction& tx, ScriptError& err)
118 {
119  err = SCRIPT_ERR_OK;
120  return VerifyScript(scriptSig, scriptPubKey, flags, MutableTransactionSignatureChecker(&tx, 0, amtIn), tx.GetRequiredSigVersion(), &err);
121 }
122 
123 BOOST_AUTO_TEST_CASE(coldstake_lof_script)
124 {
125  CScript scriptP2CS;
126  CKey stakerKey, ownerKey;
127 
128  // create unsigned coinstake transaction
129  CMutableTransaction good_tx = CreateNewColdStakeTx(scriptP2CS, stakerKey, ownerKey, true);
130 
131  // sign the input with the staker key
132  SignColdStake(good_tx, 0, scriptP2CS, stakerKey, true);
133 
134  // check the signature and script
136  CMutableTransaction tx(good_tx);
137  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
138 
139  // pay less than expected
140  tx.vout[1].nValue -= 3 * COIN;
141  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
142  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
143  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
144 
145  // Add another p2cs out
146  tx.vout.emplace_back(3 * COIN, scriptP2CS);
147  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
148  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
149 
150  const CKey& dummyKey = KeyIO::DecodeSecret("YNdsth3BsW53DYmCiR12SofWSAt2utXQUSGoin3PekVQCMbzfS7E");
151  const CKeyID& dummyKeyID = dummyKey.GetPubKey().GetID();
152  const CScript& dummyP2PKH = GetDummyP2PKH(dummyKeyID);
153 
154  // Add a masternode out
155  tx.vout.emplace_back(3 * COIN, dummyP2PKH);
156  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
157  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
158 
159  // Transfer more coins to the masternode
160  tx.vout[2].nValue -= 3 * COIN;
161  tx.vout[3].nValue += 3 * COIN;
162  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
163  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
164  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
165 
166  // Add two "free" outputs
167  tx = good_tx;
168  tx.vout[1].nValue -= 3 * COIN;
169  tx.vout.emplace_back(3 * COIN, dummyP2PKH);
170  tx.vout.emplace_back(3 * COIN, dummyP2PKH);
171  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
172  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
173  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
174  // -- but the owner can
175  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
176  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
177 
178  // Replace with new p2cs
179  tx = good_tx;
180  tx.vout[1].scriptPubKey = GetDummyP2CS(dummyKeyID);
181  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
182  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
183  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
184  // -- but the owner can
185  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
186  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
187 
188  // Replace with single dummy out
189  tx = good_tx;
190  tx.vout[1] = CTxOut(COIN, dummyP2PKH);
191  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
192  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
193  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
194  // -- but the owner can
195  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
196  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
197 }
198 
199 BOOST_AUTO_TEST_CASE(coldstake_script)
200 {
201  CScript scriptP2CS;
202  CKey stakerKey, ownerKey;
203 
204  // create unsigned coinstake transaction
205  CMutableTransaction good_tx = CreateNewColdStakeTx(scriptP2CS, stakerKey, ownerKey, false);
206 
207  // sign the input with the staker key
208  SignColdStake(good_tx, 0, scriptP2CS, stakerKey, true);
209 
210  // check the signature and script
212  CMutableTransaction tx(good_tx);
213  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
214 
215  // pay less than expected
216  tx.vout[1].nValue -= 3 * COIN;
217  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
218  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
219  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
220 
221  // Add another p2cs out
222  tx.vout.emplace_back(3 * COIN, scriptP2CS);
223  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
224  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
225 
226  const CKey& dummyKey = KeyIO::DecodeSecret("YNdsth3BsW53DYmCiR12SofWSAt2utXQUSGoin3PekVQCMbzfS7E");
227  const CKeyID& dummyKeyID = dummyKey.GetPubKey().GetID();
228  const CScript& dummyP2PKH = GetDummyP2PKH(dummyKeyID);
229 
230  // Add a dummy P2PKH out at the end
231  tx.vout.emplace_back(3 * COIN, dummyP2PKH);
232  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
233  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
234  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
235  // -- but the owner can
236  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
237  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
238 
239  // Add a dummy P2PKH out at the beginning
240  tx = good_tx;
241  tx.vout[1] = CTxOut(3 * COIN, dummyP2PKH);
242  tx.vout.emplace_back(3 * COIN, scriptP2CS);
243  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
244  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
245  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
246  // -- but the owner can
247  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
248  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
249 
250  // Replace with new p2cs
251  tx = good_tx;
252  tx.vout[1].scriptPubKey = GetDummyP2CS(dummyKeyID);
253  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
254  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
255  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
256  // -- but the owner can
257  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
258  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
259 
260  // Replace with single dummy out
261  tx = good_tx;
262  tx.vout[1] = CTxOut(COIN, dummyP2PKH);
263  SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
264  BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
265  BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
266  // -- but the owner can
267  SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
268  BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
269 }
270 
271 // Check that it's not possible to "fake" a P2CS script for the owner by splitting the locking
272 // and unlocking parts. This particular script can be spent by any key, with a
273 // unlocking script composed like: <sig> <pk> <DUP> <HASH160> <pkh>
274 static CScript GetFakeLockingScript(const CKeyID staker, const CKeyID& owner)
275 {
276  CScript script;
277  script << opcodetype(0x2F) << opcodetype(0x01) << OP_ROT <<
279  OP_ELSE << ToByteVector(owner) << OP_DROP <<
281 
282  return script;
283 }
284 
285 void FakeUnlockColdStake(CMutableTransaction& tx, const CScript& prevScript, const CKey& key)
286 {
287  // sign the first input
288  tx.vin[0].scriptSig.clear();
289  const CTransaction _tx(tx);
291  const uint256& hash = SignatureHash(prevScript, _tx, 0, SIGHASH_ALL, amtIn, sv);
292  std::vector<unsigned char> vchSig;
293  BOOST_CHECK(key.Sign(hash, vchSig));
294  vchSig.push_back((unsigned char)SIGHASH_ALL);
295  tx.vin[0].scriptSig << vchSig << ToByteVector(key.GetPubKey()) << OP_DUP << OP_HASH160 << ToByteVector(key.GetPubKey().GetID());
296 }
297 
298 static void setupWallet(CWallet& wallet)
299 {
301  wallet.SetupSPKM(false);
302 }
303 
304 BOOST_AUTO_TEST_CASE(fake_script_test)
305 {
306  CWallet& wallet = m_wallet;
307  LOCK(wallet.cs_wallet);
308  setupWallet(wallet);
309  CKey stakerKey; // dummy staker key (not in the wallet)
310  stakerKey.MakeNewKey(true);
311  CKeyID stakerId = stakerKey.GetPubKey().GetID();
312  CPubKey ownerPubKey;
313  BOOST_ASSERT(wallet.GetKeyFromPool(ownerPubKey));
314  const CKeyID& ownerId = ownerPubKey.GetID();
315  CKey ownerKey; // owner key (in the wallet)
316  BOOST_ASSERT(wallet.GetKey(ownerId, ownerKey));
317 
318  const CScript& scriptP2CS = GetFakeLockingScript(stakerId, ownerId);
319 
320  // Create prev transaction
321  CMutableTransaction txFrom;
322  txFrom.vout.resize(1);
323  txFrom.vout[0].nValue = amtIn;
324  txFrom.vout[0].scriptPubKey = scriptP2CS;
325 
326  // it does NOT pass IsPayToColdStaking
327  BOOST_CHECK_MESSAGE(!scriptP2CS.IsPayToColdStaking(), "Fake script passes as P2CS");
328 
329  // the output amount is NOT credited to the owner wallet
330  wallet.AddToWallet({&wallet, MakeTransactionRef(CTransaction(txFrom))});
332 }
333 
int64_t CAmount
Amount in PIV (Can be negative)
Definition: amount.h:13
bool GetKey(const CKeyID &address, CKey &keyOut) const override
Definition: crypter.cpp:199
An encapsulated private key.
Definition: key.h:30
void MakeNewKey(bool fCompressed)
Generate a new private key using a cryptographic PRNG.
Definition: key.cpp:158
CPubKey GetPubKey() const
Compute the public key from a private key.
Definition: key.cpp:186
bool Sign(const uint256 &hash, std::vector< unsigned char > &vchSig, uint32_t test_case=0) const
Create a DER-serialized signature.
Definition: key.cpp:200
A reference to a CKey: the Hash160 of its serialized public key.
Definition: pubkey.h:21
An encapsulated public key.
Definition: pubkey.h:44
CKeyID GetID() const
Get the KeyID of this public key (hash of its serialization)
Definition: pubkey.h:167
Serialized script, used inside transaction inputs and outputs.
Definition: script.h:381
bool IsPayToColdStaking() const
Definition: script.cpp:233
The basic transaction that is broadcasted on the network and contained in blocks.
Definition: transaction.h:244
SigVersion GetRequiredSigVersion() const
Definition: transaction.h:347
An output of a transaction.
Definition: transaction.h:137
A CWallet is an extension of a keystore, which also maintains a set of transactions and balances,...
Definition: wallet.h:577
RecursiveMutex cs_wallet
Definition: wallet.h:720
256-bit opaque blob.
Definition: uint256.h:138
BOOST_AUTO_TEST_SUITE_END()
bool GetKeyFromPool(CPubKey &key, const uint8_t &type=HDChain::ChangeType::EXTERNAL)
Definition: wallet.cpp:3752
bool SetMinVersion(enum WalletFeature, WalletBatch *batch_in=nullptr, bool fExplicit=false)
signify that a particular wallet feature is now used. this may change nWalletVersion and nWalletMaxVe...
Definition: wallet.cpp:580
bool AddToWallet(const CWalletTx &wtxIn, bool fFlushOnClose=true)
Definition: wallet.cpp:901
const CWalletTx * GetWalletTx(const uint256 &hash) const
Definition: wallet.cpp:166
bool SetupSPKM(bool newKeypool=true, bool memOnly=false)
Generates hd wallet //.
Definition: wallet.cpp:133
CAmount GetAvailableCredit(bool fUseCache=true, const isminefilter &filter=ISMINE_SPENDABLE) const
Definition: wallet.cpp:1647
bool VerifyScript(const CScript &scriptSig, const CScript &scriptPubKey, unsigned int flags, const BaseSignatureChecker &checker, SigVersion sigversion, ScriptError *serror)
uint256 SignatureHash(const CScript &scriptCode, const CTransaction &txTo, unsigned int nIn, int nHashType, const CAmount &amount, SigVersion sigversion, const PrecomputedTransactionData *cache)
@ SIGHASH_ALL
Definition: interpreter.h:24
@ ISMINE_SPENDABLE_TRANSPARENT
Definition: ismine.h:31
@ LOCK
Definition: lockunlock.h:16
CKey DecodeSecret(const std::string &str)
Definition: key_io.cpp:127
#define BOOST_FIXTURE_TEST_SUITE(a, b)
Definition: object.cpp:14
#define BOOST_CHECK_EQUAL(v1, v2)
Definition: object.cpp:18
#define BOOST_CHECK(expr)
Definition: object.cpp:17
int flags
Definition: pivx-tx.cpp:400
@ ownerKey
Definition: rpcevo.cpp:49
void setupWallet(CWallet &wallet)
std::vector< unsigned char > ToByteVector(const T &in)
Definition: script.h:43
opcodetype
Script opcodes.
Definition: script.h:50
@ OP_IF
Definition: script.h:80
@ OP_ROT
Definition: script.h:106
@ OP_CHECKSIG
Definition: script.h:166
@ OP_DUP
Definition: script.h:101
@ OP_CHECKCOLDSTAKEVERIFY
Definition: script.h:191
@ OP_FALSE
Definition: script.h:53
@ OP_HASH160
Definition: script.h:163
@ OP_TRUE
Definition: script.h:60
@ OP_DROP
Definition: script.h:100
@ OP_ELSE
Definition: script.h:84
@ OP_EQUALVERIFY
Definition: script.h:123
BOOST_AUTO_TEST_CASE(extract_cold_staking_destination_keys)
void FakeUnlockColdStake(CMutableTransaction &tx, const CScript &prevScript, const CKey &key)
void CheckValidKeyId(const CTxDestination &dest, const CKeyID &expectedKey)
void SignColdStake(CMutableTransaction &tx, int nIn, const CScript &prevScript, const CKey &key, bool fStaker)
const char * ScriptErrorString(const ScriptError serror)
Definition: script_error.cpp:9
enum ScriptError_t ScriptError
@ SCRIPT_ERR_CHECKCOLDSTAKEVERIFY
Definition: script_error.h:29
@ SCRIPT_ERR_OK
Definition: script_error.h:12
bool ExtractDestinations(const CScript &scriptPubKey, txnouttype &typeRet, std::vector< CTxDestination > &addressRet, int &nRequiredRet)
Parse a standard scriptPubKey with one or more destination addresses.
Definition: standard.cpp:195
CScript GetScriptForStakeDelegation(const CKeyID &stakingKey, const CKeyID &spendingKey)
Generate a P2CS script for the given staker and owner keys.
Definition: standard.cpp:291
CScript GetScriptForStakeDelegationLOF(const CKeyID &stakingKey, const CKeyID &spendingKey)
Definition: standard.cpp:301
CScript GetScriptForDestination(const CTxDestination &dest)
Generate a PIVX scriptPubKey for the given CTxDestination.
Definition: standard.cpp:278
bool ExtractDestination(const CScript &scriptPubKey, CTxDestination &addressRet, bool fColdStake)
Parse a standard scriptPubKey for the destination address.
Definition: standard.cpp:162
boost::variant< CNoDestination, CKeyID, CScriptID, CExchangeKeyID > CTxDestination
A txout script template with a specific destination.
Definition: standard.h:72
txnouttype
Definition: standard.h:46
@ TX_COLDSTAKE
Definition: standard.h:54
A mutable version of CTransaction.
Definition: transaction.h:409
uint256 GetHash() const
Compute the hash of this CMutableTransaction.
Definition: transaction.cpp:96
SigVersion GetRequiredSigVersion() const
Definition: transaction.h:450
std::vector< CTxOut > vout
Definition: transaction.h:411
std::vector< CTxIn > vin
Definition: transaction.h:410
SigVersion
Definition: transaction.h:26
@ FEATURE_SAPLING
Definition: wallet.h:118