Skip to content

assets: allow issuance and transaction amounts of more than MAX_MONEY for non-policy assets #1385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/asset.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 9 additions & 6 deletions src/blind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand All @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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");
}
Expand Down Expand Up @@ -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");
}
Expand Down
4 changes: 2 additions & 2 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 10 additions & 5 deletions src/wallet/receive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Expand Down Expand Up @@ -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");
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/wallet/rpc/elements.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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.");
}
Expand Down
4 changes: 2 additions & 2 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
'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',
Expand Down
94 changes: 94 additions & 0 deletions test/functional/wallet_elements_21million.py
Original file line number Diff line number Diff line change
@@ -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()