[order_utils.py] is_signature_valid, via Exchange contract (#1216)
First support for signature validation, done via Exchange contract's isValidSignature() method.
This commit is contained in:
parent
094f710662
commit
95b2898b9c
@ -162,6 +162,9 @@ jobs:
|
||||
working_directory: ~/repo
|
||||
docker:
|
||||
- image: circleci/python
|
||||
- image: 0xorg/ganache-cli
|
||||
command: |
|
||||
ganache-cli --gasLimit 10000000 --noVMErrorsOnRPCResponse --db /snapshot --noVMErrorsOnRPCResponse -p 8545 --networkId 50 -m "concert load couple harbor equip island argue ramp clarify fence smart topic"
|
||||
steps:
|
||||
- checkout
|
||||
- run: sudo chown -R circleci:circleci /usr/local/bin
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -99,6 +99,7 @@ packages/*/scripts/
|
||||
.mypy_cache
|
||||
.tox
|
||||
python-packages/*/build
|
||||
python-packages/*/dist
|
||||
__pycache__
|
||||
python-packages/*/src/*.egg-info
|
||||
python-packages/*/.coverage
|
||||
|
@ -4,6 +4,7 @@ lib
|
||||
/packages/contracts/generated-artifacts
|
||||
/packages/abi-gen-wrappers/src/generated-wrappers
|
||||
/packages/contract-artifacts/artifacts
|
||||
/python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts
|
||||
/packages/json-schemas/schemas
|
||||
/packages/metacoin/src/contract_wrappers
|
||||
/packages/metacoin/artifacts
|
||||
|
@ -18,7 +18,7 @@ Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting
|
||||
|
||||
### Install Code and Dependencies
|
||||
|
||||
Ensure that you have Python >=3.6 installed, then:
|
||||
Ensure that you have installed Python >=3.6 and Docker. Then:
|
||||
|
||||
```bash
|
||||
pip install -e .[dev]
|
||||
@ -26,7 +26,7 @@ pip install -e .[dev]
|
||||
|
||||
### Test
|
||||
|
||||
`./setup.py test`
|
||||
Tests depend on a running ganache instance with the 0x contracts deployed in it. For convenience, a docker container is provided that has ganache-cli and a snapshot containing the necessary contracts. A shortcut is provided to run that docker container: `./setup.py ganache`. With that running, the tests can be run with `./setup.py test`.
|
||||
|
||||
### Clean
|
||||
|
||||
|
@ -5,11 +5,12 @@
|
||||
import subprocess # nosec
|
||||
from shutil import rmtree
|
||||
from os import environ, path
|
||||
from pathlib import Path
|
||||
from sys import argv
|
||||
|
||||
from distutils.command.clean import clean
|
||||
import distutils.command.build_py
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
@ -59,8 +60,15 @@ class LintCommand(distutils.command.build_py.build_py):
|
||||
import eth_abi
|
||||
|
||||
eth_abi_dir = path.dirname(path.realpath(eth_abi.__file__))
|
||||
with open(path.join(eth_abi_dir, "py.typed"), "a"):
|
||||
pass
|
||||
Path(path.join(eth_abi_dir, "py.typed")).touch()
|
||||
|
||||
# HACK(gene): until eth_utils fixes
|
||||
# https://github.com/ethereum/eth-utils/issues/140 , we need to simply
|
||||
# create an empty file `py.typed` in the eth_abi package directory.
|
||||
import eth_utils
|
||||
|
||||
eth_utils_dir = path.dirname(path.realpath(eth_utils.__file__))
|
||||
Path(path.join(eth_utils_dir, "py.typed")).touch()
|
||||
|
||||
for lint_command in lint_commands:
|
||||
print(
|
||||
@ -79,7 +87,7 @@ class CleanCommandExtension(clean):
|
||||
rmtree(".mypy_cache", ignore_errors=True)
|
||||
rmtree(".tox", ignore_errors=True)
|
||||
rmtree(".pytest_cache", ignore_errors=True)
|
||||
rmtree("src/order_utils.egg-info", ignore_errors=True)
|
||||
rmtree("src/0x_order_utils.egg-info", ignore_errors=True)
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
@ -111,6 +119,26 @@ class PublishCommand(distutils.command.build_py.build_py):
|
||||
subprocess.check_call("twine upload dist/*".split()) # nosec
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class GanacheCommand(distutils.command.build_py.build_py):
|
||||
"""Custom command to publish to pypi.org."""
|
||||
|
||||
description = "Run ganache daemon to support tests."
|
||||
|
||||
def run(self):
|
||||
"""Run ganache."""
|
||||
cmd_line = (
|
||||
"docker run -d -p 8545:8545 0xorg/ganache-cli --gasLimit"
|
||||
+ " 10000000 --db /snapshot --noVMErrorsOnRPCResponse -p 8545"
|
||||
+ " --networkId 50 -m"
|
||||
).split()
|
||||
cmd_line.append(
|
||||
"concert load couple harbor equip island argue ramp clarify fence"
|
||||
+ " smart topic"
|
||||
)
|
||||
subprocess.call(cmd_line) # nosec
|
||||
|
||||
|
||||
with open("README.md", "r") as file_handle:
|
||||
README_MD = file_handle.read()
|
||||
|
||||
@ -130,9 +158,9 @@ setup(
|
||||
"test": TestCommandExtension,
|
||||
"test_publish": TestPublishCommand,
|
||||
"publish": PublishCommand,
|
||||
"ganache": GanacheCommand,
|
||||
},
|
||||
include_package_data=True,
|
||||
install_requires=["eth-abi", "mypy_extensions", "web3"],
|
||||
install_requires=["eth-abi", "eth_utils", "mypy_extensions", "web3"],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"bandit",
|
||||
@ -151,14 +179,17 @@ setup(
|
||||
]
|
||||
},
|
||||
python_requires=">=3.6, <4",
|
||||
package_data={"zero_ex.order_utils": ["py.typed"]},
|
||||
package_data={
|
||||
"zero_ex.order_utils": ["py.typed"],
|
||||
"zero_ex.contract_artifacts": ["artifacts/*"],
|
||||
},
|
||||
package_dir={"": "src"},
|
||||
license="Apache 2.0",
|
||||
keywords=(
|
||||
"ethereum cryptocurrency 0x decentralized blockchain dex exchange"
|
||||
),
|
||||
namespace_packages=["zero_ex"],
|
||||
packages=["zero_ex.order_utils", "zero_ex.dev_utils"],
|
||||
packages=find_packages("src"),
|
||||
classifiers=[
|
||||
"Development Status :: 2 - Pre-Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
|
@ -3,6 +3,7 @@
|
||||
# Reference: http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
from typing import List
|
||||
import pkg_resources
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@ -12,7 +13,7 @@ project = "0x-order-utils"
|
||||
# pylint: disable=redefined-builtin
|
||||
copyright = "2018, ZeroEx, Intl."
|
||||
author = "F. Eugene Aumson"
|
||||
version = "0.1.0" # The short X.Y version
|
||||
version = pkg_resources.get_distribution("0x-order-utils").version
|
||||
release = "" # The full version, including alpha/beta/rc tags
|
||||
|
||||
extensions = [
|
||||
|
@ -19,6 +19,9 @@ Python zero_ex.order_utils
|
||||
|
||||
See source for class properties. Sphinx does not easily generate class property docs; pull requests welcome.
|
||||
|
||||
.. automodule:: zero_ex.order_utils.signature_utils
|
||||
:members:
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
"""Solc-generated artifacts for 0x smart contracts."""
|
@ -0,0 +1 @@
|
||||
../../../../../packages/contract-artifacts/artifacts
|
@ -46,3 +46,13 @@ def assert_is_int(value: Any, name: str) -> None:
|
||||
f"expected variable '{name}', with value {str(value)}, to have"
|
||||
+ f" type 'int', not '{type(value).__name__}'"
|
||||
)
|
||||
|
||||
|
||||
def assert_is_hex_string(value: Any, name: str) -> None:
|
||||
"""Assert that :param value: is a string of hex chars.
|
||||
|
||||
If :param value: isn't a str, raise a TypeError. If it is a string but
|
||||
contains non-hex characters ("0x" prefix permitted), raise a ValueError.
|
||||
"""
|
||||
assert_is_string(value, name)
|
||||
int(value, 16) # raises a ValueError if value isn't a base-16 str
|
||||
|
@ -1 +1,11 @@
|
||||
"""Order utilities for 0x applications."""
|
||||
"""Order utilities for 0x applications.
|
||||
|
||||
Some methods require the caller to pass in a `Web3.HTTPProvider` object. For
|
||||
local testing one may construct such a provider pointing at an instance of
|
||||
`ganache-cli <https://www.npmjs.com/package/ganache-cli>`_ which has the 0x
|
||||
contracts deployed on it. For convenience, a docker container is provided for
|
||||
just this purpose. To start it: ``docker run -d -p 8545:8545 0xorg/ganache-cli
|
||||
--gasLimit 10000000 --db /snapshot --noVMErrorsOnRPCResponse -p 8545
|
||||
--networkId 50 -m "concert load couple harbor equip island argue ramp clarify
|
||||
fence smart topic"``.
|
||||
"""
|
||||
|
@ -0,0 +1,88 @@
|
||||
"""Signature utilities."""
|
||||
|
||||
from typing import Dict, Tuple
|
||||
import json
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from eth_utils import is_address, to_checksum_address
|
||||
from web3 import Web3
|
||||
import web3.exceptions
|
||||
from web3.utils import datatypes
|
||||
|
||||
from zero_ex.dev_utils.type_assertions import assert_is_hex_string
|
||||
|
||||
|
||||
# prefer `black` formatting. pylint: disable=C0330
|
||||
EXCHANGE_ABI = json.loads(
|
||||
resource_string("zero_ex.contract_artifacts", "artifacts/Exchange.json")
|
||||
)["compilerOutput"]["abi"]
|
||||
|
||||
network_to_exchange_addr: Dict[str, str] = {
|
||||
"1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b",
|
||||
"3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf",
|
||||
"42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2",
|
||||
"50": "0x48bacb9266a570d521063ef5dd96e61686dbe788",
|
||||
}
|
||||
|
||||
|
||||
# prefer `black` formatting. pylint: disable=C0330
|
||||
def is_valid_signature(
|
||||
provider: Web3.HTTPProvider, data: str, signature: str, signer_address: str
|
||||
) -> Tuple[bool, str]:
|
||||
# docstring considered all one line by pylint: disable=line-too-long
|
||||
"""Check the validity of the supplied signature.
|
||||
|
||||
Check if the supplied ``signature`` corresponds to signing ``data`` with
|
||||
the private key corresponding to ``signer_address``.
|
||||
|
||||
:param provider: A Web3 provider able to access the 0x Exchange contract.
|
||||
:param data: The hex encoded data signed by the supplied signature.
|
||||
:param signature: The hex encoded signature.
|
||||
:param signer_address: The hex encoded address that signed the data to
|
||||
produce the supplied signature.
|
||||
:rtype: Boolean indicating whether the given signature is valid.
|
||||
|
||||
>>> is_valid_signature(
|
||||
... Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
... '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0',
|
||||
... '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403',
|
||||
... '0x5409ed021d9299bf6814279a6a1411a7e866a631',
|
||||
... )
|
||||
(True, '')
|
||||
""" # noqa: E501 (line too long)
|
||||
# TODO: make this provider check more flexible. pylint: disable=fixme
|
||||
# https://app.asana.com/0/684263176955174/901300863045491/f
|
||||
if not isinstance(provider, Web3.HTTPProvider):
|
||||
raise TypeError("provider is not a Web3.HTTPProvider")
|
||||
assert_is_hex_string(data, "data")
|
||||
assert_is_hex_string(signature, "signature")
|
||||
assert_is_hex_string(signer_address, "signer_address")
|
||||
if not is_address(signer_address):
|
||||
raise ValueError("signer_address is not a valid address")
|
||||
|
||||
web3_instance = Web3(provider)
|
||||
# false positive from pylint: disable=no-member
|
||||
network_id = web3_instance.net.version
|
||||
contract_address = network_to_exchange_addr[network_id]
|
||||
# false positive from pylint: disable=no-member
|
||||
contract: datatypes.Contract = web3_instance.eth.contract(
|
||||
address=to_checksum_address(contract_address), abi=EXCHANGE_ABI
|
||||
)
|
||||
try:
|
||||
return (
|
||||
contract.call().isValidSignature(
|
||||
data, to_checksum_address(signer_address), signature
|
||||
),
|
||||
"",
|
||||
)
|
||||
except web3.exceptions.BadFunctionCallOutput as exception:
|
||||
known_revert_reasons = [
|
||||
"LENGTH_GREATER_THAN_0_REQUIRED",
|
||||
"SIGNATURE_UNSUPPORTED",
|
||||
"LENGTH_0_REQUIRED",
|
||||
"LENGTH_65_REQUIRED",
|
||||
]
|
||||
for known_revert_reason in known_revert_reasons:
|
||||
if known_revert_reason in str(exception):
|
||||
return (False, known_revert_reason)
|
||||
return (False, f"Unknown: {exception}")
|
@ -1,6 +1,8 @@
|
||||
from distutils.dist import Distribution
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
def setup(**attrs: Any) -> Distribution: ...
|
||||
|
||||
class Command: ...
|
||||
|
||||
def find_packages(where: str) -> List[str]: ...
|
||||
|
@ -1,10 +1,26 @@
|
||||
from typing import Optional, Union
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from web3.utils import datatypes
|
||||
|
||||
|
||||
class Web3:
|
||||
class HTTPProvider: ...
|
||||
|
||||
def __init__(self, provider: HTTPProvider) -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def sha3(
|
||||
primitive: Optional[Union[bytes, int, None]] = None,
|
||||
text: Optional[str] = None,
|
||||
hexstr: Optional[str] = None
|
||||
) -> bytes: ...
|
||||
|
||||
class net:
|
||||
version: str
|
||||
...
|
||||
|
||||
class eth:
|
||||
@staticmethod
|
||||
def contract(address: str, abi: Dict) -> datatypes.Contract: ...
|
||||
...
|
||||
...
|
||||
|
2
python-packages/order_utils/stubs/web3/exceptions.pyi
Normal file
2
python-packages/order_utils/stubs/web3/exceptions.pyi
Normal file
@ -0,0 +1,2 @@
|
||||
class BadFunctionCallOutput(Exception):
|
||||
...
|
@ -0,0 +1,3 @@
|
||||
class Contract:
|
||||
def call(self): ...
|
||||
...
|
@ -1,24 +1,18 @@
|
||||
"""Exercise doctests for order_utils module."""
|
||||
"""Exercise doctests for all of our modules."""
|
||||
|
||||
from doctest import testmod
|
||||
import pkgutil
|
||||
|
||||
from zero_ex.dev_utils import abi_utils, type_assertions
|
||||
from zero_ex.order_utils import asset_data_utils
|
||||
import zero_ex
|
||||
|
||||
|
||||
def test_doctest_asset_data_utils():
|
||||
"""Invoke doctest on the asset_data_utils module."""
|
||||
(failure_count, _) = testmod(asset_data_utils)
|
||||
assert failure_count == 0
|
||||
|
||||
|
||||
def test_doctest_abi_utils():
|
||||
"""Invoke doctest on the abi_utils module."""
|
||||
(failure_count, _) = testmod(abi_utils)
|
||||
assert failure_count == 0
|
||||
|
||||
|
||||
def test_doctest_type_assertions():
|
||||
"""Invoke doctest on the type_assertions module."""
|
||||
(failure_count, _) = testmod(type_assertions)
|
||||
assert failure_count == 0
|
||||
def test_all_doctests():
|
||||
"""Gather zero_ex.* modules and doctest them."""
|
||||
# prefer `black` formatting. pylint: disable=bad-continuation
|
||||
for (importer, modname, _) in pkgutil.walk_packages(
|
||||
path=zero_ex.__path__, prefix="zero_ex."
|
||||
):
|
||||
module = importer.find_module(modname).load_module(modname)
|
||||
print(module)
|
||||
(failure_count, _) = testmod(module)
|
||||
assert failure_count == 0
|
||||
|
128
python-packages/order_utils/test/test_signature_utils.py
Normal file
128
python-packages/order_utils/test/test_signature_utils.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""Tests of zero_ex.order_utils.signature_utils."""
|
||||
|
||||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
from zero_ex.order_utils.signature_utils import is_valid_signature
|
||||
|
||||
|
||||
def test_is_valid_signature__provider_wrong_type():
|
||||
"""Test that giving a non-HTTPProvider raises a TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
123,
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__data_not_string():
|
||||
"""Test that giving non-string `data` raises a TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
123,
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__data_not_hex_string():
|
||||
"""Test that giving non-hex-string `data` raises a ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"jjj",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signature_not_string():
|
||||
"""Test that passng a non-string signature raises a TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
123,
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signature_not_hex_string():
|
||||
"""Test that passing a non-hex-string signature raises a ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"jjj",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signer_address_not_string():
|
||||
"""Test that giving a non-address `signer_address` raises a ValueError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
123,
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signer_address_not_hex_string():
|
||||
"""Test that giving a non-hex-str `signer_address` raises a ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"jjj",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signer_address_not_valid_address():
|
||||
"""Test that giving a non-address for `signer_address` raises an error."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0xff",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__unsupported_sig_types():
|
||||
"""Test that passing in a sig w/invalid type raises error.
|
||||
|
||||
To induce this error, the last byte of the signature is tweaked from 03 to
|
||||
ff."""
|
||||
(is_valid, reason) = is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc334"
|
||||
+ "0349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254ff",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
assert is_valid is False
|
||||
assert reason == "SIGNATURE_UNSUPPORTED"
|
Loading…
x
Reference in New Issue
Block a user