Skip to content
Open
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
38 changes: 33 additions & 5 deletions node/rustchain_v2_integrated_v2.2.1_rip200.py
Original file line number Diff line number Diff line change
Expand Up @@ -4035,6 +4035,18 @@ def finalize_epoch(epoch, per_block_rtc, prev_block_hash: bytes = b""):
print(f"[SECURITY] No valid miners for epoch {epoch} after RIP-309 filtering")
return

# Reward settlement must work with both balance schemas that the node
# supports. Fresh/legacy databases use (miner_pk, balance_rtc), while
# migrated production databases use (miner_id, amount_i64) (optionally
# retaining balance_rtc as a display column). The old direct UPDATE
# below assumed only the latter and made enabling UTXO dual-write fail
# the whole epoch with "no such column: amount_i64" on a legacy DB.
reward_balance_cols = _balance_columns(c)
if not _supports_wallet_balance_updates(reward_balance_cols):
raise RuntimeError(
"unsupported balances schema for epoch reward settlement"
)

# ATOMIC TRANSACTION: claim the epoch first, then credit — all under an
# IMMEDIATE write lock so two concurrent finalize_epoch calls cannot both
# credit balances (double-settlement / reward inflation past the cap).
Expand Down Expand Up @@ -4089,10 +4101,26 @@ def finalize_epoch(epoch, per_block_rtc, prev_block_hash: bytes = b""):
if amount_i64 >= 2**63 or amount_nrtc >= 2**63:
raise ValueError(f"Reward overflow for miner {pk}: {amount_i64}")

upd = c.execute(
"UPDATE balances SET amount_i64 = amount_i64 + ?, balance_rtc = (amount_i64 + ?) / 1000000.0 WHERE miner_id = ?",
(amount_i64, amount_i64, pk)
)
# Preserve the existing no-phantom invariant: finalize_epoch
# must not create an account row for an enrolled miner that has
# no balance record. Once the row exists, use the
# schema-tolerant helper so legacy (miner_pk, balance_rtc) and
# modern integer-unit databases receive the same reward.
if "miner_id" in reward_balance_cols:
balance_row = c.execute(
"SELECT 1 FROM balances WHERE miner_id = ?", (pk,)
).fetchone()
else:
balance_row = c.execute(
"SELECT 1 FROM balances WHERE miner_pk = ?", (pk,)
).fetchone()
if balance_row:
_apply_wallet_balance_delta(
c, pk, amount_i64, reward_balance_cols
)
updated = 1
else:
updated = 0

# Ledger an audit row ONLY for an actual credit: amount_i64 > 0 AND the
# balance row existed (UPDATE mutated exactly one row — miner_id is the
Expand All @@ -4102,7 +4130,7 @@ def finalize_epoch(epoch, per_block_rtc, prev_block_hash: bytes = b""):
# exists to prevent. Integer truncation of tiny shares also yields
# amount_i64 == 0 (no mutation → no row). Best-effort: a ledger failure
# must not roll back the (valid) reward credit.
if amount_i64 > 0 and upd.rowcount == 1 and ledger_writable:
if amount_i64 > 0 and updated == 1 and ledger_writable:
# Best-effort audit row, isolated in a SAVEPOINT: on success it
# commits together with the credit (reconciliation intact); on
# failure ROLLBACK TO removes ONLY the failed insert and clears any
Expand Down
44 changes: 44 additions & 0 deletions node/tests/test_integrated_balance_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,50 @@ def test_finalize_epoch_writes_account_rewards_in_micro_rtc(self):
self.assertEqual(amount_i64, int(1.44 * self.mod.ACCOUNT_UNIT))
self.assertAlmostEqual(balance_rtc, 1.44)

def test_finalize_epoch_supports_legacy_balance_schema_with_utxo_dual_write(self):
"""Legacy balance tables must not make UTXO reward settlement fail."""
with closing(sqlite3.connect(self.db_path)) as db:
db.execute("DROP TABLE balances")
db.execute(
"CREATE TABLE balances ("
"miner_pk TEXT PRIMARY KEY, "
"balance_rtc REAL DEFAULT 0)"
)
db.execute(
"INSERT INTO balances (miner_pk, balance_rtc) VALUES (?, 0)",
("miner-scale",),
)
db.commit()

calls = []

class FakeUtxoDB:
def __init__(self, db_path):
self.db_path = db_path

def apply_transaction(self, tx, height, conn=None):
calls.append((tx, height, conn is not None))
return True

original_dual_write = self.mod.UTXO_DUAL_WRITE
original_utxo_db = self.mod.UtxoDB
self.mod.UTXO_DUAL_WRITE = True
self.mod.UtxoDB = FakeUtxoDB
try:
self.mod.finalize_epoch(7, 0.01, b"")
finally:
self.mod.UTXO_DUAL_WRITE = original_dual_write
self.mod.UtxoDB = original_utxo_db

with closing(sqlite3.connect(self.db_path)) as db:
balance = db.execute(
"SELECT balance_rtc FROM balances WHERE miner_pk = ?",
("miner-scale",),
).fetchone()[0]
self.assertAlmostEqual(balance, 1.44)
self.assertEqual(len(calls), 1)
self.assertTrue(calls[0][2])

def test_finalize_epoch_keeps_utxo_rewards_in_nano_rtc(self):
calls = []

Expand Down
Loading