Skip to content

Commit

Permalink
Create EthereumAddressFastBinaryField
Browse files Browse the repository at this point in the history
- Faster than `EthereumAddressBinaryField` as it does not involve
EIP55 checksums
- Depends on #835 to be merged first
  • Loading branch information
Uxio0 committed Jul 12, 2024
1 parent abe8e01 commit 0190c35
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 90 deletions.
88 changes: 40 additions & 48 deletions gnosis/eth/django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
from django.db import models
from django.utils.translation import gettext_lazy as _

from eth_typing import ChecksumAddress
from eth_typing import ChecksumAddress, HexAddress, HexStr
from eth_utils import to_normalized_address
from hexbytes import HexBytes

from ..utils import fast_bytes_to_checksum_address, fast_to_checksum_address
from .forms import EthereumAddressFieldForm, HexFieldForm, Keccak256FieldForm
from .validators import validate_checksumed_address
from .validators import validate_address, validate_checksumed_address

try:
from django.db import DefaultConnectionProxy
Expand All @@ -24,54 +24,11 @@
connection = connections["default"]


class EthereumAddressCharField(models.CharField):
"""
Stores Ethereum Addresses as Strings. Takes more space in database than `EthereumAddressBinaryField`,
but does not require the keccak256 calculation to calculate the EIP55 checksummed address.
"""

default_validators = [validate_checksumed_address]
description = "Stores Ethereum Addresses (EIP55) as strings"
default_error_messages = {
"invalid": _('"%(value)s" value must be an EIP55 checksummed address.'),
}

def __init__(self, *args, **kwargs):
kwargs["max_length"] = 42
super().__init__(*args, **kwargs)

def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs["max_length"]
return name, path, args, kwargs

def from_db_value(self, value, expression, connection):
return self.to_python(value)

def to_python(self, value):
value = super().to_python(value)
if value:
try:
return fast_to_checksum_address(value)
except ValueError:
raise exceptions.ValidationError(
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)
else:
return value

def get_prep_value(self, value):
value = super().get_prep_value(value)
return self.to_python(value)


class EthereumAddressBinaryField(models.Field):
"""
Stores Ethereum Addresses in binary. Takes less space in Database than `EthereumAddressCharField`,
but does require a keccak256 calculation to calculate the EIP55 checksummed address, that it can take
a high impact on the CPU for a lot of addresses.
Stores Ethereum Addresses in binary. Requires keccak256 hashing to
calculate the EIP55 checksummed address, and it can take a high impact
on the CPU for a lot of addresses.
"""

default_validators = [validate_checksumed_address]
Expand Down Expand Up @@ -120,6 +77,41 @@ def formfield(self, **kwargs):
return super().formfield(**defaults)


class EthereumAddressFastBinaryField(EthereumAddressBinaryField):
"""
Stores Ethereum Addresses in binary. It returns not EIP55 regular addresses,
which is faster as not EIP55 checksum is involved.
"""

default_validators = [validate_address]
description = "Stores Ethereum Addresses in binary"
default_error_messages = {
"invalid": _('"%(value)s" value must be a valid address.'),
}

def from_db_value(
self, value: memoryview, expression, connection
) -> Optional[HexAddress]:
if value:
return HexAddress(HexStr("0x" + bytes(value).hex()))

def to_python(self, value) -> Optional[ChecksumAddress]:
if value is not None:
try:
if isinstance(value, bytes):
if len(value) != 20:
raise ValueError(
"Cannot convert %s to a checksum address, 20 bytes were expected"
)
return HexAddress(HexStr(to_normalized_address(value)[2:]))
except ValueError:
raise exceptions.ValidationError(
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)


class UnsignedDecimal(models.DecimalField):
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
Expand Down
10 changes: 5 additions & 5 deletions gnosis/eth/django/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@

from ..models import (
EthereumAddressBinaryField,
EthereumAddressCharField,
EthereumAddressFastBinaryField,
Keccak256Field,
Uint32Field,
Uint96Field,
Uint256Field,
)


class EthereumAddress(models.Model):
value = EthereumAddressCharField(null=True)
class EthereumAddressBinary(models.Model):
value = EthereumAddressBinaryField(null=True)


class EthereumAddressV2(models.Model):
value = EthereumAddressBinaryField(null=True)
class EthereumAddressFastBinary(models.Model):
value = EthereumAddressFastBinaryField(null=True)


class Uint256(models.Model):
Expand Down
102 changes: 66 additions & 36 deletions gnosis/eth/django/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from ...constants import NULL_ADDRESS, SENTINEL_ADDRESS
from ...utils import fast_is_checksum_address, fast_keccak_text
from .models import (
EthereumAddress,
EthereumAddressV2,
EthereumAddressBinary,
EthereumAddressFastBinary,
Keccak256Hash,
Uint32,
Uint96,
Expand All @@ -22,38 +22,68 @@

class TestModels(TestCase):
def test_ethereum_address_field(self):
for EthereumAddressModel in (EthereumAddress, EthereumAddressV2):
with self.subTest(EthereumAddressModel=EthereumAddressModel):
address = Account.create().address
self.assertTrue(fast_is_checksum_address(address))
ethereum_address = EthereumAddressModel.objects.create(value=address)
ethereum_address.refresh_from_db()
self.assertTrue(fast_is_checksum_address(ethereum_address.value))
self.assertEqual(address, ethereum_address.value)

# Test addresses
for addresss in (
None,
NULL_ADDRESS,
SENTINEL_ADDRESS,
Account.create().address,
):
with self.subTest(special_address=addresss):
EthereumAddressModel.objects.create(value=addresss)
self.assertEqual(
EthereumAddressModel.objects.get(value=addresss).value,
addresss,
)

with self.assertRaisesMessage(
ValidationError,
'"0x23" value must be an EIP55 checksummed address.',
):
with transaction.atomic():
EthereumAddressModel.objects.create(value="0x23")

ethereum_address = EthereumAddressModel(value=Account.create().address)
self.assertIsNone(ethereum_address.full_clean())
address = Account.create().address
self.assertTrue(fast_is_checksum_address(address))
ethereum_address = EthereumAddressBinary.objects.create(value=address)
ethereum_address.refresh_from_db()
self.assertTrue(fast_is_checksum_address(ethereum_address.value))
self.assertEqual(address, ethereum_address.value)

# Test addresses
for addresss in (
None,
NULL_ADDRESS,
SENTINEL_ADDRESS,
Account.create().address,
):
with self.subTest(special_address=addresss):
EthereumAddressBinary.objects.create(value=addresss)
self.assertEqual(
EthereumAddressBinary.objects.get(value=addresss).value,
addresss,
)

with self.assertRaisesMessage(
ValidationError,
'"0x23" value must be an EIP55 checksummed address.',
):
with transaction.atomic():
EthereumAddressBinary.objects.create(value="0x23")

ethereum_address = EthereumAddressBinary(value=Account.create().address)
self.assertIsNone(ethereum_address.full_clean())

def test_ethereum_address_fast_field(self):
address = Account.create().address
self.assertTrue(fast_is_checksum_address(address))
ethereum_address = EthereumAddressFastBinary.objects.create(value=address)
ethereum_address.refresh_from_db()
self.assertFalse(fast_is_checksum_address(ethereum_address.value))
self.assertEqual(address.lower(), ethereum_address.value)

# Test addresses
for addresss in (
None,
NULL_ADDRESS,
SENTINEL_ADDRESS,
Account.create().address.lower(),
):
with self.subTest(special_address=addresss):
EthereumAddressFastBinary.objects.create(value=addresss)
self.assertEqual(
EthereumAddressFastBinary.objects.get(value=addresss).value,
addresss,
)

with self.assertRaisesMessage(
ValidationError,
'"0x23" value must be a valid address.',
):
with transaction.atomic():
EthereumAddressFastBinary.objects.create(value="0x23")

ethereum_address = EthereumAddressFastBinary(value=Account.create().address)
self.assertIsNone(ethereum_address.full_clean())

def test_uint256_field(self):
for value in [
Expand Down Expand Up @@ -177,8 +207,8 @@ def test_serialize_keccak256_field_to_json(self):

def test_serialize_ethereum_address_v2_field_to_json(self):
address: str = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe"
EthereumAddressV2.objects.create(value=address)
serialized = serialize("json", EthereumAddressV2.objects.all())
EthereumAddressBinary.objects.create(value=address)
serialized = serialize("json", EthereumAddressBinary.objects.all())
# address should be in serialized data
self.assertIn(address, serialized)

Expand Down
16 changes: 15 additions & 1 deletion gnosis/eth/django/validators.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
from django.core.exceptions import ValidationError

from hexbytes import HexBytes

from ..utils import fast_is_checksum_address


def validate_checksumed_address(address):
def validate_address(address: str):
try:
address_bytes = HexBytes(address)
if len(address_bytes) != 20:
raise ValueError
except ValueError:
raise ValidationError(
"%(address)s is not a valid EthereumAddress",
params={"address": address},
)


def validate_checksumed_address(address: str):
if not fast_is_checksum_address(address):
raise ValidationError(
"%(address)s has an invalid checksum",
Expand Down

0 comments on commit 0190c35

Please sign in to comment.