From 9f1cc945b680bde0b35313d4aa7169f9f029e980 Mon Sep 17 00:00:00 2001 From: Tom Trevethan Date: Mon, 25 Aug 2025 14:05:57 +0100 Subject: [PATCH 1/4] Fix: address error messages and test (#1486) * fix address error messages and test * newline --------- Co-authored-by: Tom Trevethan --- src/key_io.cpp | 15 ++++--- .../functional/rpc_invalid_address_message.py | 44 +++++++++---------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/key_io.cpp b/src/key_io.cpp index fb6d7719b3..c6064aa25a 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -152,11 +152,14 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par uint160 hash; error_str = ""; - // Note this will be false if it is a valid Bech32 address for a different network - bool is_bech32 = (ToLower(str.substr(0, params.Bech32HRP().size())) == params.Bech32HRP()); - bool is_blech32 = (ToLower(str.substr(0, params.Blech32HRP().size())) == params.Blech32HRP()); + bool is_bech32 = !(bech32::Decode(str).encoding == bech32::Encoding::INVALID); + bool is_blech32 = !(blech32::Decode(str).encoding == blech32::Encoding::INVALID); - if (!is_bech32 && !is_blech32 && DecodeBase58Check(str, data, 55)) { + // Note this will be false if it is a valid Bech32 address for a different network + bool is_bech32_hrp = (ToLower(str.substr(0, params.Bech32HRP().size())) == params.Bech32HRP()); + bool is_blech32_hrp = (ToLower(str.substr(0, params.Blech32HRP().size())) == params.Blech32HRP()); + + if (!is_bech32 && !is_blech32 && !is_bech32_hrp && !is_blech32_hrp && DecodeBase58Check(str, data, 55)) { // base58-encoded Bitcoin addresses. // Public-key-hash-addresses have version 0 (or 111 testnet). // The data vector contains RIPEMD160(SHA256(pubkey)), where pubkey is the serialized public key. @@ -206,14 +209,14 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par error_str = "Invalid prefix for Base58-encoded address"; } return CNoDestination(); - } else if (!is_bech32 && !is_blech32) { + } else if (!is_bech32 && !is_blech32 && !is_bech32_hrp && !is_blech32_hrp) { // Try Base58 decoding without the checksum, using a much larger max length if (!DecodeBase58(str, data, 100)) { error_str = "Not a valid Bech32 or Base58 encoding"; } else { error_str = "Invalid checksum or length of Base58 address"; } - // return CNoDestination(); // ELEMENTS: FIXME + return CNoDestination(); } data.clear(); diff --git a/test/functional/rpc_invalid_address_message.py b/test/functional/rpc_invalid_address_message.py index 36ea40517b..459e3144be 100755 --- a/test/functional/rpc_invalid_address_message.py +++ b/test/functional/rpc_invalid_address_message.py @@ -13,7 +13,7 @@ from test_framework.util import ( BECH32_VALID = 'ert1qtmp74ayg7p24uslctssvjm06q5phz4yr7gdkdv' BECH32_VALID_CAPITALS = 'ERT1QTMP74AYG7P24USLCTSSVJM06Q5PHZ4YR7GDKDV' -BECH32_VALID_MULTISIG = 'ert1qdg3myrgvzw7ml9q0ejxhlkyxm7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks' +BECH32_VALID_MULTISIG = 'ert1qmzm84udpuz6axdstxlpwafca7g7nh5w2yu8c7vqhe0rhjkcrfcfqwymvhe' BECH32_INVALID_BECH32 = 'ert1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqugsf3u' BECH32_INVALID_BECH32M = 'ert1qw508d6qejxtdg4y5r3zarvary0c5xw7kfqwaud' @@ -21,19 +21,19 @@ BECH32_INVALID_VERSION = 'ert130xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz BECH32_INVALID_SIZE = 'ert1s0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav25pltc58' BECH32_INVALID_V0_SIZE = 'ert1qw508d6qejxtdg4y5r3zarvary0c5xw7kqq2287l0' BECH32_INVALID_PREFIX = 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx' -BECH32_TOO_LONG = 'bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23m' -BECH32_ONE_ERROR = 'bcrt1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv' -BECH32_ONE_ERROR_CAPITALS = 'BCRT1QPLMTZKC2XHARPPZDLNPAQL78RSHJ68U32RAH7R' -BECH32_TWO_ERRORS = 'bcrt1qax9suht3qv95sw33xavx8crpxduefdrsvgsklu' # should be bcrt1qax9suht3qv95sw33wavx8crpxduefdrsvgsklx -BECH32_NO_SEPARATOR = 'bcrtq049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv' -BECH32_INVALID_CHAR = 'bcrt1q04oldschfnwystcqnsvyfpj23mpsg3jcedq9xv' -BECH32_MULTISIG_TWO_ERRORS = 'bcrt1qdg3myrgvzw7ml8q0ejxhlkyxn7vl9r56yzkfgvzclrf4hkpx9yfqhpsuks' -BECH32_WRONG_VERSION = 'bcrt1ptmp74ayg7p24uslctssvjm06q5phz4yrxucgnv' +BECH32_TOO_LONG = 'ert1q049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23mpsg3jcedq9xv049edschfnwystcqnsvyfpj23m' +BECH32_ONE_ERROR = 'ert1qtmp7aayg7p24uslctssvjm06q5phz4yr7gdkdv' +BECH32_ONE_ERROR_CAPITALS = 'ERT1QTMP7AAYG7P24USLCTSSVJM06Q5PHZ4YR7GDKDV' +BECH32_TWO_ERRORS = 'ert1qtmp74syg7p24uslctsavjm06q5phz4yr7gdkdv' +BECH32_NO_SEPARATOR = 'ertq049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv' +BECH32_INVALID_CHAR = 'ert1q04oldschfnwystcqnsvyfpj23mpsg3jcedq9xv' +BECH32_MULTISIG_TWO_ERRORS = 'ert1qmzm84udpua6axdstxlpwafca7g7na5w2yu8c7vqhe0rhjkcrfcfqwymvhe' +BECH32_WRONG_VERSION = 'ert1ptmp74ayg7p24uslctssvjm06q5phz4yr7gdkdv' BASE58_VALID = '2dcjQH4DQC3pMcSQkMkSQyPPEr7rZ6Ga4GR' BASE58_INVALID_PREFIX = '17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem' BASE58_INVALID_CHECKSUM = 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJJfn' -BASE58_INVALID_LENGTH = '2VKf7XKMrp4bVNVmuRbyCewkP8FhGLP2E54LHDPakr9Sq5mtU2' +BASE58_INVALID_LENGTH = '2dcjQH4DQC3pMcSQkMkSQyPPEr7rZ6Ga4GR7rZ6Ga4GR' INVALID_ADDRESS = 'asfah14i8fajz0123f' INVALID_ADDRESS_2 = '1q049ldschfnwystcqnsvyfpj23mpsg3jcedq9xv' @@ -71,36 +71,36 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): def test_validateaddress(self): # Invalid Bech32 self.check_invalid(BECH32_INVALID_SIZE, 'Invalid Bech32 address data size') - # self.check_invalid(BECH32_INVALID_PREFIX, 'Not a valid Bech32 or Base58 encoding') # ELEMENTS: FIXME + self.check_invalid(BECH32_INVALID_PREFIX, 'Invalid prefix for Bech32 address') self.check_invalid(BECH32_INVALID_BECH32, 'Version 1+ witness address must use Bech32m checksum') self.check_invalid(BECH32_INVALID_BECH32M, 'Version 0 witness address must use Bech32 checksum') self.check_invalid(BECH32_INVALID_VERSION, 'Invalid Bech32 address witness version') self.check_invalid(BECH32_INVALID_V0_SIZE, 'Invalid Bech32 v0 address data size') - self.check_invalid(BECH32_TOO_LONG, 'Bech32 string too long', list(range(90, 108))) + self.check_invalid(BECH32_TOO_LONG, 'Bech32 string too long', list(range(90, 107))) self.check_invalid(BECH32_ONE_ERROR, 'Invalid Bech32 checksum', [9]) - self.check_invalid(BECH32_TWO_ERRORS, 'Invalid Bech32 checksum', [22, 43]) - self.check_invalid(BECH32_ONE_ERROR_CAPITALS, 'Invalid Bech32 checksum', [38]) + self.check_invalid(BECH32_TWO_ERRORS, 'Invalid Bech32 checksum', [10, 23]) + self.check_invalid(BECH32_ONE_ERROR_CAPITALS, 'Invalid Bech32 checksum', [9]) self.check_invalid(BECH32_NO_SEPARATOR, 'Missing separator') - self.check_invalid(BECH32_INVALID_CHAR, 'Invalid Base 32 character', [8]) - self.check_invalid(BECH32_MULTISIG_TWO_ERRORS, 'Invalid Bech32 checksum', [19, 30]) - self.check_invalid(BECH32_WRONG_VERSION, 'Invalid Bech32 checksum', [5]) + self.check_invalid(BECH32_INVALID_CHAR, 'Invalid Base 32 character', [7]) + self.check_invalid(BECH32_MULTISIG_TWO_ERRORS, 'Invalid Bech32 checksum', [14, 33]) + self.check_invalid(BECH32_WRONG_VERSION, 'Invalid Bech32 checksum', [4]) # Valid Bech32 self.check_valid(BECH32_VALID) self.check_valid(BECH32_VALID_CAPITALS) - # self.check_valid(BECH32_VALID_MULTISIG) # ELEMENTS: FIXME + self.check_valid(BECH32_VALID_MULTISIG) # Invalid Base58 self.check_invalid(BASE58_INVALID_PREFIX, 'Invalid prefix for Base58-encoded address') - # self.check_invalid(BASE58_INVALID_CHECKSUM, 'Invalid checksum or length of Base58 address') # ELEMENTS: FIXME - # self.check_invalid(BASE58_INVALID_LENGTH, 'Invalid checksum or length of Base58 address') # ELEMENTS: FIXME + self.check_invalid(BASE58_INVALID_CHECKSUM, 'Invalid checksum or length of Base58 address') + self.check_invalid(BASE58_INVALID_LENGTH, 'Invalid checksum or length of Base58 address') # Valid Base58 self.check_valid(BASE58_VALID) # Invalid address format - # self.check_invalid(INVALID_ADDRESS, 'Not a valid Bech32 or Base58 encoding') # ELEMENTS: FIXME - # self.check_invalid(INVALID_ADDRESS_2, 'Not a valid Bech32 or Base58 encoding') # ELEMENTS: FIXME + self.check_invalid(INVALID_ADDRESS, 'Not a valid Bech32 or Base58 encoding') + self.check_invalid(INVALID_ADDRESS_2, 'Not a valid Bech32 or Base58 encoding') # ELEMENTS info = self.nodes[0].validateaddress(BLECH32_INVALID_SIZE) From e145690b42ab0a85f27c545634c0d39aa33be8c3 Mon Sep 17 00:00:00 2001 From: Tom Trevethan Date: Fri, 19 Sep 2025 11:44:39 +0100 Subject: [PATCH 2/4] apply mandatory coinbase only to policyAsset (#1488) Co-authored-by: Tom Trevethan --- src/validation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 9bad35fcc7..d2576dca3b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2148,12 +2148,12 @@ bool CChainState::ConnectBlock(const CBlock& block, BlockValidationState& state, nBlocksTotal++; - // Check that all non-zero coinbase outputs pay to the required destination + // Check that all non-zero policyAsset coinbase outputs pay to the required destination const CScript& mandatory_coinbase_destination = m_params.GetConsensus().mandatory_coinbase_destination; if (mandatory_coinbase_destination != CScript()) { for (auto& txout : block.vtx[0]->vout) { bool mustPay = !txout.nValue.IsExplicit() || txout.nValue.GetAmount() != 0; - if (mustPay && txout.scriptPubKey != mandatory_coinbase_destination) { + if (mustPay && txout.nAsset.GetAsset() == policyAsset && txout.scriptPubKey != mandatory_coinbase_destination) { LogPrintf("ERROR: ConnectBlock(): Coinbase outputs didn't match required scriptPubKey\n"); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-coinbase-txos"); } From 93c84a97f9967aff81c025624ce524f88deb563e Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 23 Sep 2025 13:17:02 +0200 Subject: [PATCH 3/4] fix: unblinded re/issuance for non-policy asset greater than 21 million (#1445) * test: add test for unblinded re/issuance greater than 21 million Co-authored-by: Mihailo Milenkovic * fix: unblinded re/issuance for greater than 21 million Co-authored-by: Mihailo Milenkovic * enable with param config * change range check to policy with config option * refactoring, additional error messages and test extension --------- Co-authored-by: Mihailo Milenkovic Co-authored-by: Tom Trevethan --- src/confidential_validation.cpp | 4 +- src/init.cpp | 2 + src/policy/policy.cpp | 14 +++++++ src/policy/policy.h | 8 ++++ src/validation.cpp | 5 +++ src/validation.h | 1 + test/functional/wallet_elements_21million.py | 42 +++++++++++++++++++- 7 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/confidential_validation.cpp b/src/confidential_validation.cpp index 05f65db3f9..a1230c609b 100644 --- a/src/confidential_validation.cpp +++ b/src/confidential_validation.cpp @@ -1,4 +1,5 @@ +#include #include #include #include @@ -103,14 +104,13 @@ static bool VerifyIssuanceAmount(secp256k1_pedersen_commitment& value_commit, se // Build value commitment if (value.IsExplicit()) { - if (!MoneyRange(value.GetAmount()) || value.GetAmount() == 0) { + if ((asset == Params().GetConsensus().pegged_asset && !MoneyRange(value.GetAmount())) || value.GetAmount() <= 0) { return false; } if (!rangeproof.empty()) { return false; } - ret = secp256k1_pedersen_commit(secp256k1_ctx_verify_amounts, &value_commit, explicit_blinds, value.GetAmount(), &asset_gen); // The explicit_blinds are all 0, and the amount is not 0. So secp256k1_pedersen_commit does not fail. assert(ret == 1); diff --git a/src/init.cpp b/src/init.cpp index 27addf275a..714b51786c 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -621,6 +621,7 @@ void SetupServerArgs(ArgsManager& argsman) std::vector elements_hidden_args = {"-con_fpowallowmindifficultyblocks", "-con_fpownoretargeting", "-con_nsubsidyhalvinginterval", "-con_bip16exception", "-con_bip34height", "-con_bip65height", "-con_bip66height", "-con_npowtargettimespan", "-con_npowtargetspacing", "-con_nrulechangeactivationthreshold", "-con_nminerconfirmationwindow", "-con_powlimit", "-con_bip34hash", "-con_nminimumchainwork", "-con_defaultassumevalid", "-npruneafterheight", "-fdefaultconsistencychecks", "-fmineblocksondemand", "-fallback_fee_enabled", "-pchmessagestart"}; + argsman.AddArg("-acceptunlimitedissuances", strprintf("Relay and mine unblinded issuance transactions (default: %u)", DEFAULT_ACCEPT_UNLIMITED_ISSUANCES), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-initialfreecoins", strprintf("The amount of OP_TRUE coins created in the genesis block. Primarily for testing. (default: %d)", 0), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-validatepegin", "Validate peg-in claims. An RPC connection will be attempted to the trusted mainchain daemon using the `mainchain*` settings below. All functionaries must run this enabled. (default: 1 if chain has federated peg)", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-mainchainrpchost=", "The address which the daemon will try to connect to the trusted mainchain daemon to validate peg-ins, if enabled. (default: 127.0.0.1)", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); @@ -1076,6 +1077,7 @@ bool AppInitParameterInteraction(const ArgsManager& args) fIsBareMultisigStd = args.GetBoolArg("-permitbaremultisig", DEFAULT_PERMIT_BAREMULTISIG); fAcceptDatacarrier = args.GetBoolArg("-datacarrier", DEFAULT_ACCEPT_DATACARRIER); nMaxDatacarrierBytes = args.GetIntArg("-datacarriersize", nMaxDatacarrierBytes); + fAcceptUnlimitedIssuances = args.GetBoolArg("-acceptunlimitedissuances", DEFAULT_ACCEPT_UNLIMITED_ISSUANCES); // Option to startup with mocktime set (used for regression testing): SetMockTime(args.GetIntArg("-mocktime", 0)); // SetMockTime(0) is a no-op diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 99435154d2..65d7bae3a5 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -308,6 +308,20 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) return true; } +bool IsIssuanceInMoneyRange(const CTransaction& tx) +{ + for (size_t i = 0; i < tx.vin.size(); ++i) { + const CAssetIssuance& issuance = tx.vin[i].assetIssuance; + if (issuance.IsNull()) { + continue; + } + if (issuance.nAmount.IsExplicit() && !MoneyRange(issuance.nAmount.GetAmount())) { + return false; + } + } + return true; +} + int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop) { return (std::max(nWeight, nSigOpCost * bytes_per_sigop) + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR; diff --git a/src/policy/policy.h b/src/policy/policy.h index 0c48bad77c..14390dcf54 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -94,6 +94,9 @@ static constexpr unsigned int STANDARD_LOCKTIME_VERIFY_FLAGS = LOCKTIME_VERIFY_S // ELEMENTS: keep a copy of the upstream default dust relay fee rate static const unsigned int DUST_RELAY_TX_FEE_BITCOIN = 3000; +// ELEMENTS: allow unblinded issuances/reissuances greater than MAX_MONEY +static const bool DEFAULT_ACCEPT_UNLIMITED_ISSUANCES = true; + CAmount GetDustThreshold(const CTxOut& txout, const CFeeRate& dustRelayFee); bool IsDust(const CTxOut& txout, const CFeeRate& dustRelayFee); @@ -126,6 +129,11 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) */ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs); +/* ELEMENTS +* Check if unblinded issuance/reissuance is in MoneyRange +*/ +bool IsIssuanceInMoneyRange(const CTransaction& tx); + /** Compute the virtual transaction size (weight reinterpreted as bytes). */ int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop); int64_t GetVirtualTransactionSize(const CTransaction& tx, int64_t nSigOpCost, unsigned int bytes_per_sigop); diff --git a/src/validation.cpp b/src/validation.cpp index d2576dca3b..a566ccd835 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -151,6 +151,7 @@ bool g_parallel_script_checks{false}; bool fRequireStandard = true; bool fCheckBlockIndex = false; bool fCheckpointsEnabled = DEFAULT_CHECKPOINTS_ENABLED; +bool fAcceptUnlimitedIssuances = true; int64_t nMaxTipAge = DEFAULT_MAX_TIP_AGE; uint256 hashAssumeValid; @@ -722,6 +723,10 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } } + // Check unblinded issuance is in MoneyRange if configured + if (!fAcceptUnlimitedIssuances && !IsIssuanceInMoneyRange(tx)) + return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "issuance-out-of-range", "Issuance is greater than 21 million and acceptunlimitedissuances is not enabled."); + // Do not work on transactions that are too small. // A transaction with 1 segwit input and 1 P2WPHK output has non-witness size of 82 bytes. // Transactions smaller than this are not relayed to mitigate CVE-2017-12842 by not relaying diff --git a/src/validation.h b/src/validation.h index 78fa8bea6a..f66ea6555b 100644 --- a/src/validation.h +++ b/src/validation.h @@ -130,6 +130,7 @@ extern bool g_parallel_script_checks; extern bool fRequireStandard; extern bool fCheckBlockIndex; extern bool fCheckpointsEnabled; +extern bool fAcceptUnlimitedIssuances; /** A fee rate smaller than this is considered zero fee (for relaying, mining and transaction creation) */ extern CFeeRate minRelayTxFee; /** If the tip is older than this (in seconds), the node is considered to be in initial block download. */ diff --git a/test/functional/wallet_elements_21million.py b/test/functional/wallet_elements_21million.py index ea6b34dd6f..063832d246 100755 --- a/test/functional/wallet_elements_21million.py +++ b/test/functional/wallet_elements_21million.py @@ -7,13 +7,21 @@ from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_raises_rpc_error, ) 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 + args = [ + "-blindedaddresses=1" + ] + self.extra_args = [ + args, + args, + args + ["-acceptunlimitedissuances=0"], # node 2 blocks unblinded issuances out of moneyrange + ] def setup_network(self, split=False): self.setup_nodes() @@ -42,6 +50,17 @@ class WalletTest(BitcoinTestFramework): self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].getbalance()[asset], 200_000_000) + self.log.info("Issue more than 21 million of a unblinded non-policy asset") + issuance = self.nodes[0].issueasset(300_000_000, 100, False) + unblinded_asset = issuance['asset'] + self.generate(self.nodes[0], 1) + assert_equal(self.nodes[0].getbalance()[unblinded_asset], 300_000_000) + + self.log.info("Reissue more than 21 million of a unblinded non-policy asset") + self.nodes[0].reissueasset(unblinded_asset, 200_000_000) + self.generate(self.nodes[0], 1) + assert_equal(self.nodes[0].getbalance()[unblinded_asset], 500_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) @@ -90,5 +109,26 @@ class WalletTest(BitcoinTestFramework): self.nodes[2].loadwallet(self.default_wallet_name) assert_equal(self.nodes[2].getbalance()[asset], 200_000_000) + # send some policy asset to node 2 for fees + addr = self.nodes[2].getnewaddress() + self.nodes[0].sendtoaddress(address=addr, amount=1) + self.generate(self.nodes[0], 1) + assert_equal(self.nodes[2].getbalance()['bitcoin'], 1) + + self.log.info("Issue more than 21 million of a non-policy asset on node 2 - rejected from mempool") + issuance = self.nodes[2].issueasset(300_000_000, 100, False) + asset = issuance['asset'] + issuance_tx = self.nodes[2].gettransaction(issuance["txid"]) + assert_raises_rpc_error(-26, "issuance-out-of-range", self.nodes[2].sendrawtransaction, issuance_tx['hex']) + self.generate(self.nodes[0], 1) + assert(asset not in self.nodes[2].getbalance()) + # transaction should be accepted on node 0 + self.nodes[0].sendrawtransaction(issuance_tx["hex"]) + assert(issuance['txid'] in self.nodes[0].getrawmempool()) + assert(issuance['txid'] not in self.nodes[2].getrawmempool()) + self.generate(self.nodes[0], 1) + assert(asset not in self.nodes[0].getbalance()) + assert_equal(self.nodes[2].getbalance()[asset], 300_000_000) + if __name__ == '__main__': WalletTest().main() From a937d53a23b42a1a99209fa5bf5c9fe1e129002e Mon Sep 17 00:00:00 2001 From: Tom Trevethan Date: Fri, 26 Sep 2025 14:15:46 +0100 Subject: [PATCH 4/4] moved acceptunlimitedissuances to chainparams and set defaults --- src/chainparams.cpp | 10 ++++++++++ src/chainparams.h | 2 ++ src/chainparamsbase.cpp | 1 + src/init.cpp | 2 -- src/validation.cpp | 4 ++-- src/validation.h | 1 - test/functional/wallet_elements_21million.py | 6 +++--- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 02ee914547..4b51aca9db 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -227,6 +227,7 @@ public: consensus.connect_genesis_outputs = false; consensus.subsidy_asset = CAsset(); anyonecanspend_aremine = false; + accept_unlimited_issuances = false; enforce_pak = false; multi_data_permitted = false; accept_discount_ct = false; @@ -374,6 +375,7 @@ public: consensus.subsidy_asset = CAsset(); anyonecanspend_aremine = false; enforce_pak = false; + accept_unlimited_issuances = false; multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; @@ -538,6 +540,7 @@ public: consensus.subsidy_asset = CAsset(); anyonecanspend_aremine = false; enforce_pak = false; + accept_unlimited_issuances = false; multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; @@ -641,6 +644,7 @@ public: consensus.subsidy_asset = CAsset(); anyonecanspend_aremine = false; enforce_pak = false; + accept_unlimited_issuances = false; multi_data_permitted = false; accept_discount_ct = false; create_discount_ct = false; @@ -909,6 +913,8 @@ protected: enforce_pak = args.GetBoolArg("-enforce_pak", false); + accept_unlimited_issuances = args.GetBoolArg("-acceptunlimitedissuances", false); + // Allow multiple op_return outputs by relay policy multi_data_permitted = args.GetBoolArg("-multi_data_permitted", enforce_pak); @@ -1167,6 +1173,8 @@ public: enforce_pak = true; + accept_unlimited_issuances = false; + multi_data_permitted = true; create_discount_ct = args.GetBoolArg("-creatediscountct", false); accept_discount_ct = args.GetBoolArg("-acceptdiscountct", true) || create_discount_ct; @@ -1525,6 +1533,8 @@ public: enforce_pak = args.GetBoolArg("-enforce_pak", enforce_pak); + accept_unlimited_issuances = false; + multi_data_permitted = args.GetBoolArg("-multi_data_permitted", multi_data_permitted); create_discount_ct = args.GetBoolArg("-creatediscountct", create_discount_ct); accept_discount_ct = args.GetBoolArg("-acceptdiscountct", accept_discount_ct) || create_discount_ct; diff --git a/src/chainparams.h b/src/chainparams.h index a80807a8eb..840a22ba5c 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -134,6 +134,7 @@ public: const std::string& ParentBech32HRP() const { return parent_bech32_hrp; } const std::string& ParentBlech32HRP() const { return parent_blech32_hrp; } bool GetEnforcePak() const { return enforce_pak; } + bool GetAcceptUnlimitedIssuances() const { return accept_unlimited_issuances; } bool GetMultiDataPermitted() const { return multi_data_permitted; } bool GetAcceptDiscountCT() const { return accept_discount_ct; } bool GetCreateDiscountCT() const { return create_discount_ct; } @@ -168,6 +169,7 @@ protected: std::string parent_bech32_hrp; std::string parent_blech32_hrp; bool enforce_pak; + bool accept_unlimited_issuances; bool multi_data_permitted; bool accept_discount_ct; bool create_discount_ct; diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index e3eb7dd45b..40a1078dbd 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -61,6 +61,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman) argsman.AddArg("-total_valid_epochs", "Per-chain parameter that sets how long a particular fedpegscript is in effect for.", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-evbparams=deployment:start:end:period:threshold", "Use given start/end times for specified version bits deployment (regtest or custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::ELEMENTS); argsman.AddArg("-con_start_p2wsh_script", "Create p2wsh addresses when starting in dynafed mode (regtest or custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::ELEMENTS); + argsman.AddArg("-acceptunlimitedissuances", "Allow unblinded issuance amounts to exceed 21 million units", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); // END ELEMENTS // } diff --git a/src/init.cpp b/src/init.cpp index 714b51786c..27addf275a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -621,7 +621,6 @@ void SetupServerArgs(ArgsManager& argsman) std::vector elements_hidden_args = {"-con_fpowallowmindifficultyblocks", "-con_fpownoretargeting", "-con_nsubsidyhalvinginterval", "-con_bip16exception", "-con_bip34height", "-con_bip65height", "-con_bip66height", "-con_npowtargettimespan", "-con_npowtargetspacing", "-con_nrulechangeactivationthreshold", "-con_nminerconfirmationwindow", "-con_powlimit", "-con_bip34hash", "-con_nminimumchainwork", "-con_defaultassumevalid", "-npruneafterheight", "-fdefaultconsistencychecks", "-fmineblocksondemand", "-fallback_fee_enabled", "-pchmessagestart"}; - argsman.AddArg("-acceptunlimitedissuances", strprintf("Relay and mine unblinded issuance transactions (default: %u)", DEFAULT_ACCEPT_UNLIMITED_ISSUANCES), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-initialfreecoins", strprintf("The amount of OP_TRUE coins created in the genesis block. Primarily for testing. (default: %d)", 0), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-validatepegin", "Validate peg-in claims. An RPC connection will be attempted to the trusted mainchain daemon using the `mainchain*` settings below. All functionaries must run this enabled. (default: 1 if chain has federated peg)", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-mainchainrpchost=", "The address which the daemon will try to connect to the trusted mainchain daemon to validate peg-ins, if enabled. (default: 127.0.0.1)", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); @@ -1077,7 +1076,6 @@ bool AppInitParameterInteraction(const ArgsManager& args) fIsBareMultisigStd = args.GetBoolArg("-permitbaremultisig", DEFAULT_PERMIT_BAREMULTISIG); fAcceptDatacarrier = args.GetBoolArg("-datacarrier", DEFAULT_ACCEPT_DATACARRIER); nMaxDatacarrierBytes = args.GetIntArg("-datacarriersize", nMaxDatacarrierBytes); - fAcceptUnlimitedIssuances = args.GetBoolArg("-acceptunlimitedissuances", DEFAULT_ACCEPT_UNLIMITED_ISSUANCES); // Option to startup with mocktime set (used for regression testing): SetMockTime(args.GetIntArg("-mocktime", 0)); // SetMockTime(0) is a no-op diff --git a/src/validation.cpp b/src/validation.cpp index a566ccd835..6ac82e3e98 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -151,7 +151,6 @@ bool g_parallel_script_checks{false}; bool fRequireStandard = true; bool fCheckBlockIndex = false; bool fCheckpointsEnabled = DEFAULT_CHECKPOINTS_ENABLED; -bool fAcceptUnlimitedIssuances = true; int64_t nMaxTipAge = DEFAULT_MAX_TIP_AGE; uint256 hashAssumeValid; @@ -724,8 +723,9 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } // Check unblinded issuance is in MoneyRange if configured - if (!fAcceptUnlimitedIssuances && !IsIssuanceInMoneyRange(tx)) + if (!chainparams.GetAcceptUnlimitedIssuances() && !IsIssuanceInMoneyRange(tx)) { return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "issuance-out-of-range", "Issuance is greater than 21 million and acceptunlimitedissuances is not enabled."); + } // Do not work on transactions that are too small. // A transaction with 1 segwit input and 1 P2WPHK output has non-witness size of 82 bytes. diff --git a/src/validation.h b/src/validation.h index f66ea6555b..78fa8bea6a 100644 --- a/src/validation.h +++ b/src/validation.h @@ -130,7 +130,6 @@ extern bool g_parallel_script_checks; extern bool fRequireStandard; extern bool fCheckBlockIndex; extern bool fCheckpointsEnabled; -extern bool fAcceptUnlimitedIssuances; /** A fee rate smaller than this is considered zero fee (for relaying, mining and transaction creation) */ extern CFeeRate minRelayTxFee; /** If the tip is older than this (in seconds), the node is considered to be in initial block download. */ diff --git a/test/functional/wallet_elements_21million.py b/test/functional/wallet_elements_21million.py index 063832d246..fb4d4a4695 100755 --- a/test/functional/wallet_elements_21million.py +++ b/test/functional/wallet_elements_21million.py @@ -18,9 +18,9 @@ class WalletTest(BitcoinTestFramework): "-blindedaddresses=1" ] self.extra_args = [ - args, - args, - args + ["-acceptunlimitedissuances=0"], # node 2 blocks unblinded issuances out of moneyrange + args + ["-acceptunlimitedissuances=1"], + args + ["-acceptunlimitedissuances=1"], + args, # node 2 blocks unblinded issuances out of moneyrange ] def setup_network(self, split=False):