Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d151c9d
fix(security): bind postgres to loopback
Starling-spell Jul 26, 2026
d4cdd2e
fix(rpc): redact validator private keys from call receipts and explor…
kriss39 Sep 14, 2026
c1ae5cc
Merge branch 'v0.123-dev' into fix/redact-private-keys-call-paths
kriss39 Sep 16, 2026
e25faed
fix(accounts): normalize address in raw-SQL balance operations
Alicepoltora Sep 22, 2026
e8e0630
fix: normalize address in sim_fundAccount
Alicepoltora Oct 12, 2026
b974e51
Merge remote-tracking branch 'origin/v0.123-dev' into prep/1789
danieljrc888 Oct 21, 2026
5d724aa
test: fix checksum expectation and session mock in fund_account norma…
danieljrc888 Oct 21, 2026
e6da79f
Merge remote-tracking branch 'origin/v0.123-dev' into prep/1773
danieljrc888 Oct 21, 2026
80e0f10
Merge remote-tracking branch 'origin/v0.123-dev' into prep/1715
danieljrc888 Oct 21, 2026
c0019c2
fix(security): bind redis and ollama to loopback
danieljrc888 Oct 21, 2026
d0bc026
Merge remote-tracking branch 'origin/v0.123-dev' into prep/1765
danieljrc888 Oct 21, 2026
a91e6bb
style: apply black to accounts manager normalization test
danieljrc888 Oct 21, 2026
e414e69
fix(explorer): redact private keys in leader_timeout_validators
danieljrc888 Oct 21, 2026
4aa1d57
Merge #1765: redact validator private keys from call receipts and exp…
danieljrc888 Oct 21, 2026
a33fb4a
Merge #1715: bind postgres, redis and ollama to loopback
danieljrc888 Oct 21, 2026
0b5e3f5
Merge #1773: normalize address in raw-SQL balance operations
danieljrc888 Oct 21, 2026
b9bc31f
Merge #1789: normalize address in sim_fundAccount
danieljrc888 Oct 21, 2026
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: 14 additions & 13 deletions backend/database_handler/accounts_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,19 @@ def create_new_account_with_address(
def is_valid_address(self, address: str) -> bool:
return is_address(address)

def get_account(self, account_address: str) -> CurrentState | None:
"""Private method to retrieve an account from the data base"""
@staticmethod
def _normalize_address(account_address: str) -> str:
"""Normalize to the checksum form used as the primary key, matching
get_account()/create_new_account_with_address(). Falls back to the
original value if checksumming fails."""
try:
normalized = to_checksum_address(account_address)
return to_checksum_address(account_address)
except Exception:
normalized = account_address
return account_address

def get_account(self, account_address: str) -> CurrentState | None:
"""Private method to retrieve an account from the data base"""
normalized = self._normalize_address(account_address)
account = (
self.session.query(CurrentState)
.filter(CurrentState.id == normalized)
Expand Down Expand Up @@ -114,27 +121,20 @@ def debit_account_balance(self, account_address: str, amount: int) -> bool:
"""Atomic conditional debit. Returns False if insufficient balance."""
if amount <= 0:
return True
try:
account_address = to_checksum_address(account_address)
except Exception:
pass
result = self.session.execute(
text(
"UPDATE current_state SET balance = balance - :amount "
"WHERE id = :addr AND balance >= :amount"
),
{"amount": amount, "addr": account_address},
{"amount": amount, "addr": self._normalize_address(account_address)},
)
return result.rowcount > 0

def credit_account_balance(self, account_address: str, amount: int):
"""Atomic credit. Creates account if it doesn't exist."""
if amount <= 0:
return
try:
account_address = to_checksum_address(account_address)
except Exception:
pass
account_address = self._normalize_address(account_address)
self.session.execute(
text(
"INSERT INTO current_state (id, data, balance) "
Expand All @@ -158,6 +158,7 @@ def credit_tx_value_once(
Sets value_credited=true atomically. Returns True if credit was applied."""
if amount <= 0:
return False
target_address = self._normalize_address(target_address)
# Ensure target account exists
self.session.execute(
text(
Expand Down
52 changes: 15 additions & 37 deletions backend/protocol_rpc/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
NotFoundError,
QueueDepthExceeded,
)
from backend.protocol_rpc.redaction import (
sanitize_rpc_private_keys as _sanitize_rpc_private_keys,
)
from backend.protocol_rpc.contract_storage_quota import (
enforce_contract_storage_quota,
live_state_column_size,
Expand Down Expand Up @@ -167,40 +170,6 @@ def __init__(
_gen_call_singleflight_lock = asyncio.Lock()


def _show_validator_private_keys_in_rpc() -> bool:
return os.getenv("SHOW_VALIDATOR_PRIVATE_KEYS_IN_RPC", "").strip().lower() in {
"1",
"true",
"yes",
"on",
}


def _is_private_key_field(key: Any) -> bool:
if not isinstance(key, str):
return False
normalized = key.replace("_", "").replace("-", "").lower()
return normalized == "privatekey" or normalized.endswith("privatekey")


def _sanitize_rpc_private_keys(value: Any) -> Any:
"""Return RPC data with private-key fields removed unless explicitly enabled."""
if _show_validator_private_keys_in_rpc():
return value

if isinstance(value, dict):
return {
key: _sanitize_rpc_private_keys(item)
for key, item in value.items()
if not _is_private_key_field(key)
}
if isinstance(value, list):
return [_sanitize_rpc_private_keys(item) for item in value]
if isinstance(value, tuple):
return tuple(_sanitize_rpc_private_keys(item) for item in value)
return value


def _check_rate_limit(address: str) -> None:
"""Reject if address exceeds rate limit. Prunes old entries."""
now = time.monotonic()
Expand Down Expand Up @@ -575,6 +544,8 @@ def fund_account(
if normalized_amount <= 0:
raise InvalidParams(message="amount must be a positive integer")

account_address = eth_utils.to_checksum_address(account_address)

import secrets

nonce = transactions_processor.get_transaction_count(None)
Expand Down Expand Up @@ -1616,7 +1587,9 @@ async def sim_call(
genvm_manager,
params,
)
return TransactionsProcessor._json_safe_numbers(receipt.to_dict())
return TransactionsProcessor._json_safe_numbers(
_sanitize_rpc_private_keys(receipt.to_dict())
)


async def sim_estimate_transaction_fees(
Expand Down Expand Up @@ -1920,7 +1893,10 @@ async def execute(active_fee_accounting):
raise JSONRPCError(
code=-32000,
message="execution failed",
data={"receipt": receipt.to_dict(), "params": params},
data={
"receipt": _sanitize_rpc_private_keys(receipt.to_dict()),
"params": params,
},
)

return receipt
Expand Down Expand Up @@ -2474,7 +2450,9 @@ async def eth_call(

if receipt.execution_result != ExecutionResultStatus.SUCCESS:
raise JSONRPCError(
code=-32000, message="execution failed", data={"receipt": receipt.to_dict()}
code=-32000,
message="execution failed",
data={"receipt": _sanitize_rpc_private_keys(receipt.to_dict())},
)
return eth_utils.hexadecimal.encode_hex(receipt.result[1:])

Expand Down
9 changes: 6 additions & 3 deletions backend/protocol_rpc/explorer/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
TransactionStatus,
Validators,
)
from backend.protocol_rpc.redaction import sanitize_rpc_private_keys


def _serialize_tx(
Expand All @@ -30,7 +31,7 @@ def _serialize_tx(
"to_address": tx.to_address,
"input_data": tx.input_data,
"data": tx.data,
"consensus_data": tx.consensus_data,
"consensus_data": sanitize_rpc_private_keys(tx.consensus_data),
"nonce": tx.nonce,
"value": tx.value,
"type": tx.type,
Expand All @@ -42,14 +43,16 @@ def _serialize_tx(
"s": tx.s,
"v": tx.v,
"appeal_failed": tx.appeal_failed,
"consensus_history": tx.consensus_history,
"consensus_history": sanitize_rpc_private_keys(tx.consensus_history),
"timestamp_appeal": tx.timestamp_appeal,
"appeal_processing_time": tx.appeal_processing_time,
"config_rotation_rounds": tx.config_rotation_rounds,
"num_of_initial_validators": tx.num_of_initial_validators,
"last_vote_timestamp": tx.last_vote_timestamp,
"rotation_count": tx.rotation_count,
"leader_timeout_validators": tx.leader_timeout_validators,
"leader_timeout_validators": sanitize_rpc_private_keys(
tx.leader_timeout_validators
),
"sim_config": tx.sim_config,
"triggered_by_hash": tx.triggered_by_hash,
"triggered_on": tx.triggered_on,
Expand Down
44 changes: 44 additions & 0 deletions backend/protocol_rpc/redaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Redaction of validator private keys from RPC-facing payloads.

Receipts carry the executing validator's ``node_config`` (including its
``private_key``); anything that returns a receipt to a client must pass
through :func:`sanitize_rpc_private_keys`. Shared by the JSON-RPC endpoints and
the explorer REST API.
"""

import os
from typing import Any


def show_validator_private_keys_in_rpc() -> bool:
return os.getenv("SHOW_VALIDATOR_PRIVATE_KEYS_IN_RPC", "").strip().lower() in {
"1",
"true",
"yes",
"on",
}


def is_private_key_field(key: Any) -> bool:
if not isinstance(key, str):
return False
normalized = key.replace("_", "").replace("-", "").lower()
return normalized == "privatekey" or normalized.endswith("privatekey")


def sanitize_rpc_private_keys(value: Any) -> Any:
"""Return RPC data with private-key fields removed unless explicitly enabled."""
if show_validator_private_keys_in_rpc():
return value

if isinstance(value, dict):
return {
key: sanitize_rpc_private_keys(item)
for key, item in value.items()
if not is_private_key_field(key)
}
if isinstance(value, list):
return [sanitize_rpc_private_keys(item) for item in value]
if isinstance(value, tuple):
return tuple(sanitize_rpc_private_keys(item) for item in value)
return value
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ services:
profiles: ["ollama"]
image: ollama/ollama:0.17.6
ports:
- 11434:11434
- "127.0.0.1:11434:11434"
volumes:
- ./.ollama:/root/.ollama
container_name: ollama
Expand All @@ -275,7 +275,7 @@ services:
image: postgres:16-alpine
command: sh -c "if [ \"$REMOTE_DATABASE\" = \"true\" ]; then echo 'Postgres disabled in hosted environment' && exec tail -f /dev/null; else exec docker-entrypoint.sh postgres; fi"
ports:
- "${DBHOSTPORT:-5432}:5432"
- "127.0.0.1:${DBHOSTPORT:-5432}:5432"
environment:
- POSTGRES_USER=${DBUSER}
- POSTGRES_PASSWORD=${DBPASSWORD}
Expand Down Expand Up @@ -429,7 +429,7 @@ services:
redis:
image: redis:8-alpine
ports:
- "${REDISPORT:-6379}:6379"
- "127.0.0.1:${REDISPORT:-6379}:6379"
volumes:
- redis_data:/data
healthcheck:
Expand Down
Loading
Loading