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
11 changes: 9 additions & 2 deletions bounties/issue-2296/src/cross_node_replay_defense.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,14 +302,21 @@ def store_used_cross_node_nonce(
expires_at = now_ts + CROSS_NODE_NONCE_TTL

try:
conn.execute(
cleanup_expired_nonces(conn, now_ts)

cursor = conn.execute(
"""
INSERT OR REPLACE INTO cross_node_nonces
INSERT OR IGNORE INTO cross_node_nonces
(nonce, miner_id, node_id, first_seen, expires_at, attestation_hash, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(nonce, miner_id, NODE_ID, now_ts, expires_at, attestation_hash, now_ts)
)
if cursor.rowcount == 0:
conn.commit()
log.warning(f"Nonce {nonce[:16]}... already tracked; preserving original owner")
return False

conn.commit()

log.debug(f"Stored nonce {nonce[:16]}... for miner {miner_id}")
Expand Down
47 changes: 47 additions & 0 deletions bounties/issue-2296/tests/test_cross_node_replay_defense.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,53 @@ def test_miner_binding_invariant(self, test_db):
assert valid is False
assert error == "nonce_belongs_to_different_miner"

def test_store_preserves_first_active_nonce_owner(self, test_db):
"""Storing an already-active nonce must not replace its first owner."""
nonce = "nonce_first_owner_invariant"

with patch('cross_node_replay_defense.NODE_ID', "node-original"):
first_result = store_used_cross_node_nonce(test_db, nonce, "miner_original")

with patch('cross_node_replay_defense.NODE_ID', "node-replay"):
second_result = store_used_cross_node_nonce(test_db, nonce, "miner_replay")

row = test_db.execute(
"""
SELECT miner_id, node_id
FROM cross_node_nonces
WHERE nonce = ?
""",
(nonce,),
).fetchone()

assert first_result is True
assert second_result is False
assert row == ("miner_original", "node-original")

def test_store_can_reuse_nonce_after_expiration(self, test_db):
"""Expired records are cleaned before a fresh nonce owner is stored."""
nonce = "nonce_expired_then_reused"
past_time = 1700000000
future_time = past_time + CROSS_NODE_NONCE_TTL + 100

with patch('cross_node_replay_defense.NODE_ID', "node-old"):
store_used_cross_node_nonce(test_db, nonce, "miner_old", now_ts=past_time)

with patch('cross_node_replay_defense.NODE_ID', "node-new"):
result = store_used_cross_node_nonce(test_db, nonce, "miner_new", now_ts=future_time)

row = test_db.execute(
"""
SELECT miner_id, node_id
FROM cross_node_nonces
WHERE nonce = ?
""",
(nonce,),
).fetchone()

assert result is True
assert row == ("miner_new", "node-new")


# =============================================================================
# Regression Tests
Expand Down
Loading