Skip to content

Commit 068d93c

Browse files
ArokyaMatthewScottcjn
authored andcommitted
[UTXO-BUG] HIGH-2: Unbounded mining_reward — no output cap on minting transactions
When tx_type='mining_reward' and inputs=[], the conservation check is completely skipped (line 347: 'if inputs and ...' is falsy). Any code that can call apply_transaction() with tx_type='mining_reward' can mint arbitrary amounts with no upper bound. Fix: add MAX_COINBASE_OUTPUT_NRTC = 150 * UNIT (1.5 RTC, matching the block reward) and reject minting transactions whose output_total exceeds this cap. Tests added: - test_mining_reward_at_cap_allowed (boundary) - test_mining_reward_over_cap_rejected (demonstrates the bug) Existing test updated: - test_mempool_block_candidates: lowered coinbase from 200 to 120 RTC to stay within the new cap All 36 tests pass. Bounty: #2819 (High, 100 RTC)
1 parent 4bc2f64 commit 068d93c

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

node/test_utxo_db.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from utxo_db import (
1515
UtxoDB, coin_select, compute_box_id, address_to_proposition,
16-
proposition_to_address, UNIT, DUST_THRESHOLD,
16+
proposition_to_address, UNIT, DUST_THRESHOLD, MAX_COINBASE_OUTPUT_NRTC,
1717
)
1818

1919

@@ -317,7 +317,7 @@ def test_mempool_add_and_remove(self):
317317

318318
def test_mempool_block_candidates(self):
319319
self._apply_coinbase('alice', 100 * UNIT, block_height=1)
320-
self._apply_coinbase('alice', 200 * UNIT, block_height=2)
320+
self._apply_coinbase('alice', 120 * UNIT, block_height=2)
321321
boxes = self.db.get_unspent_for_address('alice')
322322

323323
# Add two txs with different fees (outputs + fee <= inputs)
@@ -330,7 +330,7 @@ def test_mempool_block_candidates(self):
330330
self.db.mempool_add({
331331
'tx_id': 'high' * 16,
332332
'inputs': [{'box_id': boxes[1]['box_id']}],
333-
'outputs': [{'address': 'bob', 'value_nrtc': 200 * UNIT - 5000}],
333+
'outputs': [{'address': 'bob', 'value_nrtc': 120 * UNIT - 5000}],
334334
'fee_nrtc': 5000,
335335
})
336336

@@ -445,6 +445,27 @@ def test_spending_proof_accepted_without_verification(self):
445445
self.assertTrue(ok, "UTXO layer should accept any spending_proof "
446446
"(verification is endpoint's job)")
447447

448+
def test_mining_reward_at_cap_allowed(self):
449+
"""Mining reward exactly at MAX_COINBASE_OUTPUT_NRTC must succeed."""
450+
ok = self._apply_coinbase('miner', MAX_COINBASE_OUTPUT_NRTC)
451+
self.assertTrue(ok)
452+
self.assertEqual(self.db.get_balance('miner'), MAX_COINBASE_OUTPUT_NRTC)
453+
454+
def test_mining_reward_over_cap_rejected(self):
455+
"""Mining reward exceeding MAX_COINBASE_OUTPUT_NRTC must be rejected.
456+
Without this, any caller that passes tx_type='mining_reward' can
457+
mint unlimited funds (bounty #2819 HIGH-2)."""
458+
ok = self.db.apply_transaction({
459+
'tx_type': 'mining_reward',
460+
'inputs': [],
461+
'outputs': [{'address': 'attacker',
462+
'value_nrtc': MAX_COINBASE_OUTPUT_NRTC + 1}],
463+
'fee_nrtc': 0,
464+
'timestamp': int(time.time()),
465+
}, block_height=10)
466+
self.assertFalse(ok)
467+
self.assertEqual(self.db.get_balance('attacker'), 0)
468+
448469
def test_mempool_empty_inputs_rejected_for_transfer(self):
449470
"""Mempool must also reject non-minting txs with empty inputs."""
450471
tx = {

node/utxo_db.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
UNIT = 100_000_000 # 1 RTC = 100,000,000 nanoRTC (8 decimals)
2929
DUST_THRESHOLD = 1_000 # nanoRTC below which change is absorbed into fee
30+
MAX_COINBASE_OUTPUT_NRTC = 150 * UNIT # Max minting output per block (1.5 RTC)
3031
MAX_POOL_SIZE = 10_000
3132
MAX_TX_AGE_SECONDS = 3_600 # 1 hour mempool expiry
3233
P2PK_PREFIX = b'\x00\x08' # Pay-to-Public-Key proposition prefix
@@ -397,6 +398,13 @@ def apply_transaction(self, tx: dict, block_height: int,
397398
conn.execute("ROLLBACK")
398399
return False
399400

401+
# Cap minting (coinbase) output to prevent unbounded fund creation.
402+
# Without this, any caller that passes tx_type='mining_reward'
403+
# can mint arbitrary amounts.
404+
if tx_type in MINTING_TX_TYPES and output_total > MAX_COINBASE_OUTPUT_NRTC:
405+
conn.execute("ROLLBACK")
406+
return False
407+
400408
if fee < 0:
401409
conn.execute("ROLLBACK")
402410
return False

0 commit comments

Comments
 (0)