Skip to content
Closed
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
88 changes: 88 additions & 0 deletions node/test_utxo_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,93 @@ def test_transfer_with_fee(self):
self.assertEqual(data['change_rtc'], 9.0)


class TestDualWriteConsistency(unittest.TestCase):
"""Verify that dual-write uses the same UNIT as the UTXO layer.

Bounty #2819 CRIT-2: the original code multiplied by 1e9 instead of
UNIT (1e8), inflating the account model by 10x on every transfer.
"""

def setUp(self):
self.tmp = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
self.tmp.close()
self.db_path = self.tmp.name

import sqlite3
conn = sqlite3.connect(self.db_path)
conn.execute(
"CREATE TABLE IF NOT EXISTS balances "
"(miner_id TEXT PRIMARY KEY, amount_i64 INTEGER DEFAULT 0)"
)
conn.execute(
"CREATE TABLE IF NOT EXISTS ledger "
"(ts INTEGER, epoch INTEGER, miner_id TEXT, "
" delta_i64 INTEGER, reason TEXT)"
)
# Seed sender balance in account model
conn.execute(
"INSERT INTO balances (miner_id, amount_i64) VALUES (?, ?)",
('RTC_test_aabbccdd', 100 * UNIT),
)
conn.commit()
conn.close()

self.utxo_db = UtxoDB(self.db_path)
self.utxo_db.init_tables()

self.app = Flask(__name__)
self.app.config['TESTING'] = True
register_utxo_blueprint(
self.app, self.utxo_db, self.db_path,
verify_sig_fn=mock_verify_sig,
addr_from_pk_fn=mock_addr_from_pk,
current_slot_fn=mock_current_slot,
dual_write=True, # <-- dual-write ON
)
self.client = self.app.test_client()

def tearDown(self):
os.unlink(self.db_path)

def test_dual_write_amount_matches_utxo(self):
"""Account-model delta must equal UTXO-model delta (same UNIT)."""
# Seed UTXO
self.utxo_db.apply_transaction({
'tx_type': 'mining_reward',
'inputs': [],
'outputs': [{'address': 'RTC_test_aabbccdd',
'value_nrtc': 100 * UNIT}],
'timestamp': int(time.time()),
}, block_height=1)

# Transfer 60 RTC via the endpoint (dual-write enabled)
r = self.client.post('/utxo/transfer', json={
'from_address': 'RTC_test_aabbccdd',
'to_address': 'bob',
'amount_rtc': 60.0,
'public_key': 'aabbccdd' * 8,
'signature': 'sig' * 22,
'nonce': int(time.time() * 1000),
})
self.assertEqual(r.status_code, 200)

# Check account model got the right amount
import sqlite3
conn = sqlite3.connect(self.db_path)
row = conn.execute(
"SELECT amount_i64 FROM balances WHERE miner_id = ?",
('bob',),
).fetchone()
conn.close()

expected = 60 * UNIT # 6_000_000_000 nanoRTC
self.assertIsNotNone(row, "bob should exist in balances table")
self.assertEqual(
row[0], expected,
f"Account model got {row[0]} but UTXO model transferred "
f"{expected} — decimal mismatch!"
)


if __name__ == '__main__':
unittest.main()
6 changes: 5 additions & 1 deletion node/utxo_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,11 @@ def utxo_transfer():
try:
conn = sqlite3.connect(_db_path)
c = conn.cursor()
amount_i64 = int(amount_rtc * 1_000_000_000) # account model uses 9 decimals
# Use amount_nrtc (already converted via UNIT = 10^8) to stay
# consistent with the UTXO model. The previous code used
# int(amount_rtc * 1_000_000_000) which is 10x too large,
# inflating the account model on every dual-write transfer.
amount_i64 = amount_nrtc
c.execute("INSERT OR IGNORE INTO balances (miner_id, amount_i64) VALUES (?, 0)",
(to_address,))
c.execute("UPDATE balances SET amount_i64 = amount_i64 - ? WHERE miner_id = ?",
Expand Down