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
2 changes: 1 addition & 1 deletion node/beacon_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ def update_contract(contract_id):
return jsonify({'error': 'Missing X-Agent-Key header — authentication required'}), 401

from_agent = contract['from_agent']
to_agent = contract.get('to_agent', '')
to_agent = contract['to_agent']

# Caller must be either the from_agent or to_agent
if agent_key != from_agent and agent_key != to_agent:
Expand Down
3 changes: 2 additions & 1 deletion node/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"""

import hashlib
import hmac
import json
import logging
import sqlite3
Expand Down Expand Up @@ -553,7 +554,7 @@ def founder_veto(proposal_id: int):
# Admin key is validated via environment variable (not hardcoded)
import os
expected_key = os.environ.get("RUSTCHAIN_ADMIN_KEY", "")
if not expected_key or admin_key != expected_key:
if not expected_key or not hmac.compare_digest(admin_key, expected_key):
return jsonify({"error": "invalid admin_key"}), 403

try:
Expand Down
17 changes: 17 additions & 0 deletions node/rewards_implementation_rip200.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,23 @@ def settle_epoch_rip200(db_path, epoch: int, enable_anti_double_mining: bool = T
return result
except Exception as e:
print(f"[WARN] Anti-double-mining failed, falling back to standard: {e}")
# SECURITY FIX: Rollback any partial writes from the failed
# anti-double-mining path before falling through. Without this,
# the standard path would append its own writes on top of the
# uncommitted partial state, causing a double-spend when commit()
# is called at the end.
try:
db.rollback()
except Exception:
pass
# Re-acquire the transaction lock after rollback
db.execute("BEGIN IMMEDIATE")
# Re-check settled status — another worker may have settled
# while we were rolling back.
st = db.execute("SELECT settled FROM epoch_state WHERE epoch=?", (epoch,)).fetchone()
if st and int(st[0]) == 1:
db.rollback()
return {"ok": True, "epoch": epoch, "already_settled": True}
# Fall through to standard rewards

# Standard RIP-200 rewards (no anti-double-mining)
Expand Down
2 changes: 1 addition & 1 deletion node/rustchain_sync_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def require_admin(f):
@wraps(f)
def decorated(*args, **kwargs):
key = request.headers.get("X-Admin-Key") or request.headers.get("X-API-Key")
if not key or key != admin_key:
if not key or not hmac.compare_digest(key, admin_key):
return jsonify({"error": "Unauthorized"}), 401
return f(*args, **kwargs)

Expand Down
3 changes: 2 additions & 1 deletion node/sophia_attestation_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import time
import sqlite3
import hashlib
import hmac
import argparse
import logging
import traceback
Expand Down Expand Up @@ -701,7 +702,7 @@ def register_sophia_endpoints(app, db_path: str = None):
def _is_admin(req):
need = os.environ.get("RC_ADMIN_KEY", "")
got = req.headers.get("X-Admin-Key", "") or req.headers.get("X-API-Key", "")
return bool(need and got and need == got)
return bool(need and got and hmac.compare_digest(got, need))

@app.route("/sophia/status/<miner_id>", methods=["GET"])
def sophia_status_miner(miner_id):
Expand Down
3 changes: 2 additions & 1 deletion node/sophia_governor_review_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from __future__ import annotations

import hmac
import json
import os
import re
Expand Down Expand Up @@ -142,7 +143,7 @@ def _is_authorized(req) -> bool:
required_admin = os.getenv("RC_ADMIN_KEY", "").strip()
if required_admin:
provided_admin = (req.headers.get("X-Admin-Key") or req.headers.get("X-API-Key") or "").strip()
if provided_admin == required_admin:
if hmac.compare_digest(provided_admin, required_admin):
return True

auth_header = (req.headers.get("Authorization") or "").strip()
Expand Down
53 changes: 38 additions & 15 deletions tests/test_beacon_atlas_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ def setUpClass(cls):
cls.app.config['DB_PATH'] = cls.test_db_path

# Import blueprint routes manually to avoid teardown_appcontext issue
from node.beacon_api import DB_PATH, init_beacon_tables

from node import beacon_api as beacon_module
from node.beacon_api import init_beacon_tables
beacon_module.DB_PATH = cls.test_db_path

# Register blueprint
from node import beacon_api as beacon_module
cls.app.register_blueprint(beacon_module.beacon_api)
Expand All @@ -53,7 +55,24 @@ def teardown_request(exception):

# Initialize database tables
init_beacon_tables(cls.test_db_path)


# Seed relay_agents so from_agent lookup in create_contract succeeds
with sqlite3.connect(cls.test_db_path) as conn:
now = int(time.time())
test_agents = [
('bcn_alice_test', 'deadbeef' * 8, 'Alice', 'active', None, now, now),
('bcn_bob_test', 'cafecafe' * 8, 'Bob', 'active', None, now, now),
('bcn_test_from', 'facb00fb' * 8, 'From', 'active', None, now, now),
('bcn_test_to', 'beefbabe' * 8, 'To', 'active', None, now, now),
]
conn.executemany(
"INSERT OR IGNORE INTO relay_agents "
"(agent_id, pubkey_hex, name, status, coinbase_address, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
test_agents,
)
conn.commit()

cls.client = cls.app.test_client()

@classmethod
Expand Down Expand Up @@ -95,30 +114,32 @@ def test_create_contract_workflow(self):
create_response = self.client.post(
'/api/contracts',
data=json.dumps(contract_data),
content_type='application/json'
content_type='application/json',
headers={'X-Agent-Key': 'bcn_alice_test'},
)
self.assertEqual(create_response.status_code, 201)

created = json.loads(create_response.data)
self.assertIn('id', created)
self.assertEqual(created['from'], 'bcn_alice_test')
self.assertEqual(created['to'], 'bcn_bob_test')
self.assertEqual(created['state'], 'offered')

contract_id = created['id']

# Verify contract appears in list
list_response = self.client.get('/api/contracts')
self.assertEqual(list_response.status_code, 200)
contracts = json.loads(list_response.data)
self.assertEqual(len(contracts), 1)
self.assertEqual(contracts[0]['id'], contract_id)
# Update contract state to active

# Update contract state to active (only to_agent can accept)
update_response = self.client.put(
f'/api/contracts/{contract_id}',
data=json.dumps({'state': 'active'}),
content_type='application/json'
content_type='application/json',
headers={'X-Agent-Key': 'bcn_bob_test'},
)
self.assertEqual(update_response.status_code, 200)

Expand Down Expand Up @@ -249,15 +270,17 @@ def test_invalid_state_update_rejected(self):
create_response = self.client.post(
'/api/contracts',
data=json.dumps(contract_data),
content_type='application/json'
content_type='application/json',
headers={'X-Agent-Key': 'bcn_test_from'},
)
contract_id = json.loads(create_response.data)['id']
# Try invalid state

# Try invalid state (caller must be from_agent or to_agent)
update_response = self.client.put(
f'/api/contracts/{contract_id}',
data=json.dumps({'state': 'invalid_state'}),
content_type='application/json'
content_type='application/json',
headers={'X-Agent-Key': 'bcn_test_from'},
)
self.assertEqual(update_response.status_code, 400)

Expand Down Expand Up @@ -489,4 +512,4 @@ def run_tests():


if __name__ == "__main__":
sys.exit(run_tests())
sys.exit(run_tests())
Loading