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
26 changes: 25 additions & 1 deletion node/bridge_api.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed #6629 against the Ethereum address validation bug. This is focused: 2 files changed, the new ethereum branch applies the same 0x/42-char/hex validation shape as Base, and the added tests cover garbage, missing prefix, wrong length, non-hex, valid lowercase, mixed case, zero address, and unchanged Base/RustChain behavior. I ran python -m pytest node/test_bridge_ethereum_address_poc.py -q and got 10 passed, and python -m py_compile node/bridge_api.py node/test_bridge_ethereum_address_poc.py passed. No blocking findings from this pass.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import os
import re
from typing import Optional, Tuple, Dict, Any
from Crypto.Hash import keccak as _keccak
from decimal import Decimal, InvalidOperation
from dataclasses import dataclass
from enum import Enum
Expand Down Expand Up @@ -119,6 +120,14 @@ class ValidationResult:
VALID_BRIDGE_TYPES = {"bottube", "internal", "custom"}


def _eip55_checksum(address_hex: str) -> str:
"""Return the EIP-55 checksummed form of a lowercase 40-char hex address."""
k = _keccak.new(digest_bits=256)
k.update(address_hex.encode("ascii"))
h = k.hexdigest()
return "".join(c.upper() if int(h[i], 16) >= 8 else c for i, c in enumerate(address_hex))


def validate_bridge_request(data: Optional[Dict]) -> ValidationResult:
"""Validate bridge transfer request payload."""
if not data:
Expand Down Expand Up @@ -268,7 +277,22 @@ def validate_chain_address_format(chain: str, address: str) -> Tuple[bool, str]:
return False, "Invalid Base address length"
if not all(char in "0123456789abcdefABCDEF" for char in address[2:]):
return False, "Invalid Base address hex"


elif chain == "ethereum":
# Ethereum addresses: 0x + 40 hex chars.
# All-lowercase and all-uppercase are accepted without checksum.
# Mixed-case must satisfy EIP-55 checksum to catch typos in payout addresses.
if not address.startswith("0x"):
return False, "Ethereum addresses must start with '0x'"
if len(address) != 42:
return False, "Invalid Ethereum address length"
hex_part = address[2:]
if not all(char in "0123456789abcdefABCDEF" for char in hex_part):
return False, "Invalid Ethereum address hex"
is_mixed = any(c.islower() for c in hex_part) and any(c.isupper() for c in hex_part)
if is_mixed and address[2:] != _eip55_checksum(hex_part.lower()):
return False, "Mixed-case Ethereum address fails EIP-55 checksum — use all-lowercase or a valid checksummed address"

return True, ""


Expand Down
77 changes: 77 additions & 0 deletions node/test_bridge_ethereum_address_poc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
PoC: validate_chain_address_format accepted any non-empty string >= 10 chars
for the 'ethereum' chain because no elif branch existed.

Before fix: validate_chain_address_format("ethereum", "garbage123") -> (True, "")
After fix: validate_chain_address_format("ethereum", "garbage123") -> (False, "...")
"""
import unittest

try:
from bridge_api import validate_chain_address_format
except ImportError:
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from bridge_api import validate_chain_address_format


class TestBridgeEthereumAddressValidation(unittest.TestCase):

# --- rejection cases (all were previously accepted) ---

def test_rejects_garbage_string(self):
ok, msg = validate_chain_address_format("ethereum", "garbage1234567890")
self.assertFalse(ok, "Garbage string must be rejected")

def test_rejects_missing_0x_prefix(self):
ok, msg = validate_chain_address_format("ethereum", "aabbccddeeff00112233445566778899aabbccdd")
self.assertFalse(ok)
self.assertIn("0x", msg)

def test_rejects_wrong_length(self):
ok, msg = validate_chain_address_format("ethereum", "0xdeadbeef")
self.assertFalse(ok)

def test_rejects_non_hex_chars(self):
ok, msg = validate_chain_address_format("ethereum", "0x" + "g" * 40)
self.assertFalse(ok)

def test_rejects_empty(self):
ok, msg = validate_chain_address_format("ethereum", "")
self.assertFalse(ok)

# --- acceptance cases ---

def test_accepts_valid_lowercase(self):
ok, _ = validate_chain_address_format("ethereum", "0x" + "a" * 40)
self.assertTrue(ok)

def test_accepts_valid_eip55_checksum(self):
# EIP-55 checksummed address — mixed-case must match keccak-derived checksum
ok, _ = validate_chain_address_format("ethereum", "0xabCDEF1234567890ABcDEF1234567890aBCDeF12")
self.assertTrue(ok)

def test_rejects_invalid_mixed_case(self):
# Typo'd mixed-case that does not satisfy EIP-55 checksum
ok, msg = validate_chain_address_format("ethereum", "0xAbCdEf1234567890AbCdEf1234567890AbCdEf12")
self.assertFalse(ok)
self.assertIn("EIP-55", msg)

def test_accepts_zero_address(self):
ok, _ = validate_chain_address_format("ethereum", "0x" + "0" * 40)
self.assertTrue(ok)

# --- other chains unaffected ---

def test_base_still_validated(self):
ok, msg = validate_chain_address_format("base", "not-an-address")
self.assertFalse(ok)

def test_rustchain_unaffected(self):
ok, _ = validate_chain_address_format("rustchain", "RTC" + "a" * 40)
self.assertTrue(ok)


if __name__ == "__main__":
unittest.main()
Loading