Skip to content
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
27 changes: 24 additions & 3 deletions node/test_utxo_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from utxo_db import (
UtxoDB, coin_select, compute_box_id, address_to_proposition,
proposition_to_address, UNIT, DUST_THRESHOLD,
proposition_to_address, UNIT, DUST_THRESHOLD, MAX_COINBASE_OUTPUT_NRTC,
)


Expand Down Expand Up @@ -317,7 +317,7 @@ def test_mempool_add_and_remove(self):

def test_mempool_block_candidates(self):
self._apply_coinbase('alice', 100 * UNIT, block_height=1)
self._apply_coinbase('alice', 200 * UNIT, block_height=2)
self._apply_coinbase('alice', 120 * UNIT, block_height=2)
boxes = self.db.get_unspent_for_address('alice')

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

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

def test_mining_reward_at_cap_allowed(self):
"""Mining reward exactly at MAX_COINBASE_OUTPUT_NRTC must succeed."""
ok = self._apply_coinbase('miner', MAX_COINBASE_OUTPUT_NRTC)
self.assertTrue(ok)
self.assertEqual(self.db.get_balance('miner'), MAX_COINBASE_OUTPUT_NRTC)

def test_mining_reward_over_cap_rejected(self):
"""Mining reward exceeding MAX_COINBASE_OUTPUT_NRTC must be rejected.
Without this, any caller that passes tx_type='mining_reward' can
mint unlimited funds (bounty #2819 HIGH-2)."""
ok = self.db.apply_transaction({
'tx_type': 'mining_reward',
'inputs': [],
'outputs': [{'address': 'attacker',
'value_nrtc': MAX_COINBASE_OUTPUT_NRTC + 1}],
'fee_nrtc': 0,
'timestamp': int(time.time()),
}, block_height=10)
self.assertFalse(ok)
self.assertEqual(self.db.get_balance('attacker'), 0)

def test_mempool_empty_inputs_rejected_for_transfer(self):
"""Mempool must also reject non-minting txs with empty inputs."""
tx = {
Expand Down
8 changes: 8 additions & 0 deletions node/utxo_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

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

# Cap minting (coinbase) output to prevent unbounded fund creation.
# Without this, any caller that passes tx_type='mining_reward'
# can mint arbitrary amounts.
if tx_type in MINTING_TX_TYPES and output_total > MAX_COINBASE_OUTPUT_NRTC:
conn.execute("ROLLBACK")
return False

if fee < 0:
conn.execute("ROLLBACK")
return False
Expand Down
Loading