Merge pull request #1385 from delta1/wallet-receive-gt-21million

assets: allow issuance and transaction amounts of more than MAX_MONEY for non-policy assets
This commit is contained in:
Pablo Greco 2025-01-23 05:44:33 -08:00 committed by GitHub
commit e9a914465d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 134 additions and 26 deletions

View file

@ -90,15 +90,19 @@ bool operator==(const CAmountMap& a, const CAmountMap& b);
bool operator!=(const CAmountMap& a, const CAmountMap& b);
bool operator!(const CAmountMap& a); // Check if all values are 0
inline bool MoneyRange(const CAmountMap& mapValue) {
inline bool MoneyRange(const CAmountMap& mapValue, const CAsset& pegged_asset) {
for(CAmountMap::const_iterator it = mapValue.begin(); it != mapValue.end(); it++) {
if (it->second < 0 || it->second > MAX_MONEY) {
if (it->second < 0 || ((pegged_asset.IsNull() || it->first == pegged_asset) && it->second > MAX_MONEY)) {
return false;
}
}
return true;
}
inline bool MoneyRange(const CAmountMap& mapValue) {
return MoneyRange(mapValue, CAsset());
}
CAmount valueFor(const CAmountMap& mapValue, const CAsset& asset);
std::ostream& operator<<(std::ostream& out, const CAmountMap& map);

View file

@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <blind.h>
#include <chainparams.h>
#include <hash.h>
#include <primitives/transaction.h>
@ -157,11 +158,6 @@ bool UnblindConfidentialPair(const CKey& blinding_key, const CConfidentialValue&
return false;
}
// Value sidechannel must be a transaction-valid amount (should be belt-and-suspenders check)
if (amount > (uint64_t)MAX_MONEY || !MoneyRange((CAmount)amount)) {
return false;
}
// Convenience pointers to starting point of each recovered 32 byte message
unsigned char *asset_type = msg;
unsigned char *asset_blinder = msg+32;
@ -172,6 +168,13 @@ bool UnblindConfidentialPair(const CKey& blinding_key, const CConfidentialValue&
return false;
}
CAsset asset{std::vector<unsigned char>{asset_type, asset_type + 32}};
// Value sidechannel must be a transaction-valid amount (should be belt-and-suspenders check)
if ((!committedScript.IsUnspendable() && amount == 0) || (asset == Params().GetConsensus().pegged_asset && (amount > (uint64_t)MAX_MONEY || !MoneyRange((CAmount)amount)))) {
return false;
}
// Serialize both generators then compare
unsigned char observed_generator[33];
unsigned char derived_generator[33];
@ -182,7 +185,7 @@ bool UnblindConfidentialPair(const CKey& blinding_key, const CConfidentialValue&
}
amount_out = (CAmount)amount;
asset_out = CAsset(std::vector<unsigned char>(asset_type, asset_type+32));
asset_out = asset;
asset_blinding_factor_out = uint256(std::vector<unsigned char>(asset_blinder, asset_blinder+32));
return true;
}

View file

@ -3049,7 +3049,7 @@ static RPCHelpMan rawissueasset()
CAmount asset_amount = 0;
const UniValue& asset_amount_uni = issuance_o["asset_amount"];
if (asset_amount_uni.isNum()) {
asset_amount = AmountFromValue(asset_amount_uni);
asset_amount = AmountFromValue(asset_amount_uni, false);
if (asset_amount <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, asset_amount must be positive");
}
@ -3066,7 +3066,7 @@ static RPCHelpMan rawissueasset()
CAmount token_amount = 0;
const UniValue& token_amount_uni = issuance_o["token_amount"];
if (token_amount_uni.isNum()) {
token_amount = AmountFromValue(token_amount_uni);
token_amount = AmountFromValue(token_amount_uni, false);
if (token_amount <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, token_amount must be positive");
}
@ -3177,7 +3177,7 @@ static RPCHelpMan rawreissueasset()
CAmount asset_amount = 0;
const UniValue& asset_amount_uni = issuance_o["asset_amount"];
if (asset_amount_uni.isNum()) {
asset_amount = AmountFromValue(asset_amount_uni);
asset_amount = AmountFromValue(asset_amount_uni, false);
if (asset_amount <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, asset_amount must be positive");
}

View file

@ -90,14 +90,14 @@ void RPCTypeCheckObj(const UniValue& o,
}
}
CAmount AmountFromValue(const UniValue& value, int decimals)
CAmount AmountFromValue(const UniValue& value, bool check_range, int decimals)
{
if (!value.isNum() && !value.isStr())
throw JSONRPCError(RPC_TYPE_ERROR, "Amount is not a number or string");
CAmount amount;
if (!ParseFixedPoint(value.getValStr(), decimals, &amount))
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
if (!MoneyRange(amount))
if (amount < 0 || (check_range && !MoneyRange(amount)))
throw JSONRPCError(RPC_TYPE_ERROR, "Amount out of range");
return amount;
}

View file

@ -91,7 +91,7 @@ std::vector<unsigned char> ParseHexO(const UniValue& o, std::string strKey);
* @param[in] decimals Number of significant digits (default: 8).
* @returns a CAmount if the various checks pass.
*/
CAmount AmountFromValue(const UniValue& value, int decimals = 8);
CAmount AmountFromValue(const UniValue& value, bool check_range = true, int decimals = 8);
using RPCArgList = std::vector<std::pair<std::string, UniValue>>;
std::string HelpExampleCli(const std::string& methodname, const std::string& args);

View file

@ -70,14 +70,17 @@ CAmountMap TxGetCredit(const CWallet& wallet, const CWalletTx& wtx, const ismine
{
LOCK(wallet.cs_wallet);
CAsset pegged_asset{Params().GetConsensus().pegged_asset};
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
if (wallet.IsMine(wtx.tx->vout[i]) & filter) {
CAsset asset{wtx.GetOutputAsset(wallet, i)};
CAmount credit = std::max<CAmount>(0, wtx.GetOutputValueOut(wallet, i));
if (!MoneyRange(credit))
if (asset == pegged_asset && !MoneyRange(credit)) {
throw std::runtime_error(std::string(__func__) + ": value out of range");
}
nCredit[wtx.GetOutputAsset(wallet, i)] += credit;
if (!MoneyRange(nCredit))
nCredit[asset] += credit;
if (!MoneyRange(nCredit, pegged_asset))
throw std::runtime_error(std::string(__func__) + ": value out of range");
}
}
@ -226,16 +229,18 @@ CAmountMap CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wt
bool allow_used_addresses = (filter & ISMINE_USED) || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
CAmountMap nCredit;
uint256 hashTx = wtx.GetHash();
CAsset pegged_asset{Params().GetConsensus().pegged_asset};
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++)
{
if (!wallet.IsSpent(hashTx, i) && (allow_used_addresses || !wallet.IsSpentKey(hashTx, i))) {
if (wallet.IsMine(wtx.tx->vout[i]) & filter) {
CAsset asset = wtx.GetOutputAsset(wallet, i);
CAmount credit = std::max<CAmount>(0, wtx.GetOutputValueOut(wallet, i));
if (!MoneyRange(credit))
if (asset == pegged_asset && !MoneyRange(credit))
throw std::runtime_error(std::string(__func__) + ": value out of range");
nCredit[wtx.GetOutputAsset(wallet, i)] += std::max<CAmount>(0, wtx.GetOutputValueOut(wallet, i));
if (!MoneyRange(nCredit))
if (!MoneyRange(nCredit, pegged_asset))
throw std::runtime_error(std::string(__func__) + ": value out of range");
}
}

View file

@ -1426,8 +1426,8 @@ RPCHelpMan issueasset()
throw JSONRPCError(RPC_TYPE_ERROR, "Issuance can only be done on elements-style chains. Note: `-regtest` is Bitcoin's regtest mode, instead try `-chain=<custom chain name>`");
}
CAmount nAmount = AmountFromValue(request.params[0]);
CAmount nTokens = AmountFromValue(request.params[1]);
CAmount nAmount = AmountFromValue(request.params[0], false);
CAmount nTokens = AmountFromValue(request.params[1], false);
if (nAmount == 0 && nTokens == 0) {
throw JSONRPCError(RPC_TYPE_ERROR, "Issuance must have one non-zero component");
}
@ -1524,7 +1524,7 @@ RPCHelpMan reissueasset()
std::string assetstr = request.params[0].get_str();
CAsset asset = GetAssetFromString(assetstr);
CAmount nAmount = AmountFromValue(request.params[1]);
CAmount nAmount = AmountFromValue(request.params[1], false);
if (nAmount <= 0) {
throw JSONRPCError(RPC_TYPE_ERROR, "Reissuance must create a non-zero amount.");
}

View file

@ -50,7 +50,7 @@ static void ParseRecipients(const UniValue& address_amounts, const UniValue& add
destinations.insert(dest);
CScript script_pub_key = GetScriptForDestination(dest);
CAmount amount = AmountFromValue(address_amounts[i++]);
CAmount amount = AmountFromValue(address_amounts[i++], asset == Params().GetConsensus().pegged_asset);
bool subtract_fee = false;
for (unsigned int idx = 0; idx < subtract_fee_outputs.size(); idx++) {
@ -124,7 +124,7 @@ static void SetFeeEstimateMode(const CWallet& wallet, CCoinControl& cc, const Un
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and fee_rate");
}
// Fee rates in sat/vB cannot represent more than 3 significant digits.
cc.m_feerate = CFeeRate{AmountFromValue(fee_rate, /* decimals */ 3)};
cc.m_feerate = CFeeRate{AmountFromValue(fee_rate, /*check_range=*/true, /*decimals=*/3)};
if (override_min_fee) cc.fOverrideFeeRate = true;
// Default RBF to true for explicit fee_rate, if unset.
if (!cc.m_signal_bip125_rbf) cc.m_signal_bip125_rbf = true;

View file

@ -265,8 +265,9 @@ void AvailableCoins(const CWallet& wallet, std::vector<COutput> &vCoins, const C
if (asset_filter && asset != *asset_filter) {
continue;
}
if (outValue < nMinimumAmount || outValue > nMaximumAmount)
if (outValue < nMinimumAmount || (asset == Params().GetConsensus().pegged_asset && outValue > nMaximumAmount)) {
continue;
}
if (coinControl && coinControl->HasSelected() && !coinControl->fAllowOtherInputs && !coinControl->IsSelected(COutPoint(entry.first, i)))
continue;

View file

@ -1418,7 +1418,7 @@ CAmountMap CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter)
for (const CTxIn& txin : tx.vin)
{
nDebit += GetDebit(txin, filter);
if (!MoneyRange(nDebit))
if (!MoneyRange(nDebit, Params().GetConsensus().pegged_asset))
throw std::runtime_error(std::string(__func__) + ": value out of range");
}
return nDebit;

View file

@ -112,6 +112,7 @@ BASE_SCRIPTS = [
'rpc_getnewblockhex.py',
'wallet_elements_regression_1172.py --legacy-wallet',
'wallet_elements_regression_1259.py --legacy-wallet',
'wallet_elements_21million.py',
'feature_trim_headers.py',
# Longest test should go first, to favor running tests in parallel
'wallet_hd.py --legacy-wallet',

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# Copyright (c) 2017-2020 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
)
class WalletTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 3
self.extra_args = [['-blindedaddresses=1']] * self.num_nodes
def setup_network(self, split=False):
self.setup_nodes()
self.connect_nodes(0, 1)
self.connect_nodes(1, 2)
self.connect_nodes(0, 2)
self.sync_all()
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
self.generate(self.nodes[0], COINBASE_MATURITY + 1)
assert_equal(self.nodes[0].getbalance(), {'bitcoin': 50})
assert_equal(self.nodes[1].getbalance(), {'bitcoin': 0})
self.log.info("Issue more than 21 million of a non-policy asset")
issuance = self.nodes[0].issueasset(100_000_000, 100)
asset = issuance['asset']
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getbalance()[asset], 100_000_000)
self.log.info("Reissue more than 21 million of a non-policy asset")
self.nodes[0].reissueasset(asset, 100_000_000)
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getbalance()[asset], 200_000_000)
# send more than 21 million of that asset
addr = self.nodes[1].getnewaddress()
self.nodes[0].sendtoaddress(address=addr, amount=22_000_000, assetlabel=asset)
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getbalance()[asset], 178_000_000)
assert_equal(self.nodes[1].getbalance()[asset], 22_000_000)
# unload/load wallet
self.nodes[1].unloadwallet("")
self.nodes[1].loadwallet("")
assert_equal(self.nodes[1].getbalance()[asset], 22_000_000)
# send more than 45 million of that asset
addr = self.nodes[2].getnewaddress()
self.nodes[0].sendtoaddress(address=addr, amount=46_000_000, assetlabel=asset)
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getbalance()[asset], 132_000_000)
assert_equal(self.nodes[2].getbalance()[asset], 46_000_000)
# unload/load wallet
self.nodes[2].unloadwallet("")
self.nodes[2].loadwallet("")
assert_equal(self.nodes[2].getbalance()[asset], 46_000_000)
# send some policy asset to node 1 for fees
addr = self.nodes[1].getnewaddress()
self.nodes[0].sendtoaddress(address=addr, amount=1)
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[1].getbalance()['bitcoin'], 1)
assert_equal(self.nodes[1].getbalance()[asset], 22_000_000)
# send the remainders
addr = self.nodes[2].getnewaddress()
self.nodes[0].sendtoaddress(address=addr, amount=132_000_000, assetlabel=asset)
addr = self.nodes[2].getnewaddress()
self.nodes[1].sendtoaddress(address=addr, amount=22_000_000, assetlabel=asset)
self.sync_mempools()
self.generate(self.nodes[0], 1)
assert asset not in self.nodes[0].getbalance()
assert asset not in self.nodes[1].getbalance()
assert_equal(self.nodes[2].getbalance()[asset], 200_000_000)
# unload/load wallet
self.nodes[2].unloadwallet("")
self.nodes[2].loadwallet("")
assert_equal(self.nodes[2].getbalance()[asset], 200_000_000)
if __name__ == '__main__':
WalletTest().main()