add reissuance token moneyrange check
What changed, and why it matters
This commit fixes a missing validation check in the Elements sidechain. When creating or reissuing custom assets, the code already checked that the main asset amount was within the 21-million-coin money range, but it forgot to perform the same check on the optional 'reissuance token' amount. A user could therefore issue or carry a reissuance token whose nominal supply exceeded the normal limit. The patch adds that missing range check and includes tests showing that nodes with stricter policy now reject such out-of-range reissuance-token issuances from their memory pool.
Treat as a low-to-moderate policy-layer bug fix. Review whether any consensus-critical path also relies on IsIssuanceInMoneyRange and confirm the fix is backported to maintained branches. No emergency response is indicated, but nodes should upgrade to enforce consistent issuance policy.
Security signals we found
Missing input/issuance validation
Policy bypass / inconsistency
Money-range overflow-like condition
Functional tests added for rejection behavior
Evidence from the diff
In src/policy/policy.cpp, IsIssuanceInMoneyRange() now also validates issuance.nInflationKeys when it is explicit and non-null, returning false if it is outside MoneyRange. Previously only issuance.nAmount was checked. The functional test wallet_elements_21million.py is extended to demonstrate that a node running default policy (acceptunlimitedissuances=0) rejects transactions whose reissuance token issuance exceeds the money range, while a node with acceptunlimitedissuances=1 still accepts them. This is a policy-layer consistency fix, not a consensus change.
Changed components
src/policy/policy.cppIsIssuanceInMoneyRange()CTxOut::assetIssuance.nInflationKeys handlingtest/functional/wallet_elements_21million.pyInspect captured patch +46 / −1
diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp
index 65d7bae..b88cb53 100644
--- a/src/policy/policy.cpp
+++ b/src/policy/policy.cpp
@@ -318,6 +318,10 @@ bool IsIssuanceInMoneyRange(const CTransaction& tx)
if (issuance.nAmount.IsExplicit() && !MoneyRange(issuance.nAmount.GetAmount())) {
return false;
}
+ // check the reissuance token is in range
+ if (!issuance.nInflationKeys.IsNull() && issuance.nInflationKeys.IsExplicit() && !MoneyRange(issuance.nInflationKeys.GetAmount())) {
+ return false;
+ }
}
return true;
}
diff --git a/test/functional/wallet_elements_21million.py b/test/functional/wallet_elements_21million.py
index fb4d4a4..73ef7fe 100755
--- a/test/functional/wallet_elements_21million.py
+++ b/test/functional/wallet_elements_21million.py
@@ -20,7 +20,7 @@ class WalletTest(BitcoinTestFramework):
self.extra_args = [
args + ["-acceptunlimitedissuances=1"],
args + ["-acceptunlimitedissuances=1"],
- args, # node 2 blocks unblinded issuances out of moneyrange
+ args, # node 2 blocks unblinded issuances out of moneyrange (default -acceptunlimitedissuances=0)
]
def setup_network(self, split=False):
@@ -61,6 +61,12 @@ class WalletTest(BitcoinTestFramework):
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getbalance()[unblinded_asset], 500_000_000)
+ self.log.info("Issue more than 21 million of a unblinded reissuance token")
+ issuance = self.nodes[0].issueasset(300_000_000, 100_000_000, False)
+ self.generate(self.nodes[0], 1)
+ assert_equal(self.nodes[0].getbalance()[issuance['asset']], 300_000_000)
+ assert_equal(self.nodes[0].getbalance()[issuance['token']], 100_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)
@@ -130,5 +136,40 @@ class WalletTest(BitcoinTestFramework):
assert(asset not in self.nodes[0].getbalance())
assert_equal(self.nodes[2].getbalance()[asset], 300_000_000)
+ self.log.info("Reissue more than 21 million of a unblinded non-policy asset on node 2 - rejected from mempool")
+ issuance = self.nodes[2].issueasset(3_000_000, 100, False)
+ asset = issuance['asset']
+ unblinded_asset = issuance['asset']
+ self.generate(self.nodes[2], 1)
+ assert_equal(self.nodes[2].getbalance()[unblinded_asset], 3_000_000)
+ reissuance = self.nodes[2].reissueasset(unblinded_asset, 200_000_000)
+ reissuance_tx = self.nodes[2].gettransaction(reissuance["txid"])
+ assert_raises_rpc_error(-26, "issuance-out-of-range", self.nodes[2].sendrawtransaction, reissuance_tx['hex'])
+ # transaction should be accepted on node 0
+ self.nodes[0].sendrawtransaction(reissuance_tx["hex"])
+ assert(reissuance['txid'] in self.nodes[0].getrawmempool())
+ assert(reissuance['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], 203_000_000)
+
+ self.log.info("Issue more than 21 million reissuance tokens on node 2 - rejected from mempool")
+ issuance = self.nodes[2].issueasset(3_000_000, 200_000_000, False)
+ asset = issuance['asset']
+ token = issuance['token']
+ 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())
+ assert(token 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], 3_000_000)
+ assert_equal(self.nodes[2].getbalance()[token], 200_000_000)
+
if __name__ == '__main__':
WalletTest().main()
Why this scored 60/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.