Skip to content

Commit

Permalink
Merge pull request #56 from Bhargavasomu/implement_eip_191
Browse files Browse the repository at this point in the history
Implement signing data with intended validator as part of EIP 191
  • Loading branch information
pipermerriam committed Mar 11, 2019
2 parents 692943d + 321d676 commit 7be70c3
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 14 deletions.
4 changes: 3 additions & 1 deletion eth_account/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from eth_account.account import Account # noqa: F401
from eth_account.account import ( # noqa: F401
Account,
)
33 changes: 29 additions & 4 deletions eth_account/_utils/signing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from cytoolz import (
curry,
pipe,
)
from eth_utils import (
Expand All @@ -17,6 +18,10 @@
CHAIN_ID_OFFSET = 35
V_OFFSET = 27

# signature versions
PERSONAL_SIGN_VERSION = b'E' # Hex value 0x45
INTENDED_VALIDATOR_SIGN_VERSION = b'\x00' # Hex value 0x00


def sign_transaction_dict(eth_key, transaction_dict):
# generate RLP-serializable transaction, with defaults filled
Expand All @@ -40,14 +45,34 @@ def sign_transaction_dict(eth_key, transaction_dict):


# watch here for updates to signature format: https://github.com/ethereum/EIPs/issues/191
def signature_wrapper(message, version=b'E'):
assert isinstance(message, bytes)
if version == b'E':
@curry
def signature_wrapper(message, signature_version, version_specific_data):
if not isinstance(message, bytes):
raise TypeError("Message is of the type {}, expected bytes".format(type(message)))
if not isinstance(signature_version, bytes):
raise TypeError("Signature Version is of the type {}, expected bytes".format(
type(signature_version))
)

if signature_version == PERSONAL_SIGN_VERSION:
preamble = b'\x19Ethereum Signed Message:\n'
size = str(len(message)).encode('utf-8')
return preamble + size + message
elif signature_version == INTENDED_VALIDATOR_SIGN_VERSION:
wallet_address = to_bytes(hexstr=version_specific_data)
if len(wallet_address) != 20:
raise TypeError("Invalid Wallet Address: {}".format(version_specific_data))
wrapped_message = b'\x19' + signature_version + wallet_address + message
return wrapped_message
else:
raise NotImplementedError("Only the 'Ethereum Signed Message' preamble is supported")
raise NotImplementedError(
"Currently supported signature versions are: {0}, {1}. ".
format(
'0x' + INTENDED_VALIDATOR_SIGN_VERSION.hex(),
'0x' + PERSONAL_SIGN_VERSION.hex()
) +
"But received signature version {}".format('0x' + signature_version.hex())
)


def hash_of_signed_transaction(txn_obj):
Expand Down
40 changes: 31 additions & 9 deletions eth_account/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,33 @@
)


def defunct_hash_message(primitive=None, hexstr=None, text=None):
def defunct_hash_message(
primitive=None,
*,
hexstr=None,
text=None,
signature_version=b'E',
version_specific_data=None):
'''
Convert the provided message into a message hash, to be signed.
This provides the same prefix and hashing approach as
:meth:`w3.eth.sign() <web3.eth.Eth.sign>`. That means that the
message will automatically be prepended with text
defined in EIP-191 as version 'E': ``b'\\x19Ethereum Signed Message:\\n'``
concatenated with the number of bytes in the message.
:meth:`w3.eth.sign() <web3.eth.Eth.sign>`.
Currently you can only specify the ``signature_version`` as following.
* **Version** ``0x45`` (version ``E``): ``b'\\x19Ethereum Signed Message:\\n'``
concatenated with the number of bytes in the message.
.. note:: This is the defualt version followed, if the signature_version is not specified.
* **Version** ``0x00`` (version ``0``): Sign data with intended validator (EIP 191).
Here the version_specific_data would be a hexstr which is the 20 bytes account address
of the intended validator.
Awkwardly, the number of bytes in the message is encoded in decimal ascii. So
if the message is 'abcde', then the length is encoded as the ascii
For version ``0x45`` (version ``E``), Awkwardly, the number of bytes in the message is
encoded in decimal ascii. So if the message is 'abcde', then the length is encoded as the ascii
character '5'. This is one of the reasons that this message format is not preferred.
There is ambiguity when the message '00' is encoded, for example.
Only use this method if you must have compatibility with
Only use this method with version ``E`` if you must have compatibility with
:meth:`w3.eth.sign() <web3.eth.Eth.sign>`.
Supply exactly one of the three arguments:
Expand All @@ -37,6 +50,8 @@ def defunct_hash_message(primitive=None, hexstr=None, text=None):
:type primitive: bytes or int
:param str hexstr: the message encoded as hex
:param str text: the message as a series of unicode characters (a normal Py3 str)
:param bytes signature_version: a byte indicating which kind of prefix is to be added (EIP 191)
:param version_specific_data: the data which is related to the prefix (EIP 191)
:returns: The hash of the message, after adding the prefix
:rtype: ~hexbytes.main.HexBytes
Expand Down Expand Up @@ -64,5 +79,12 @@ def defunct_hash_message(primitive=None, hexstr=None, text=None):
HexBytes('0x1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750')
'''
message_bytes = to_bytes(primitive, hexstr=hexstr, text=text)
recovery_hasher = compose(HexBytes, keccak, signature_wrapper)
recovery_hasher = compose(
HexBytes,
keccak,
signature_wrapper(
signature_version=signature_version,
version_specific_data=version_specific_data,
)
)
return recovery_hasher(message_bytes)
50 changes: 50 additions & 0 deletions tests/core/test_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ def acct(request):
return Account


@pytest.fixture(params=("text", "primative", "hexstr"))
def signature_kwargs(request):
if request == "text":
return {"text": "hello world"}
elif request == "primative":
return {"primative": b"hello world"}
else:
return {"hexstr": "68656c6c6f20776f726c64"}


def test_eth_account_default_kdf(acct, monkeypatch):
assert os.getenv('ETH_ACCOUNT_KDF') is None
assert acct.default_kdf == 'scrypt'
Expand Down Expand Up @@ -322,6 +332,46 @@ def test_eth_account_sign(acct, message, key, expected_bytes, expected_hash, v,
assert account.signHash(msghash) == signed


def test_eth_valid_account_address_sign_data_with_intended_validator(acct, signature_kwargs):
account = acct.create()
hashed_msg = defunct_hash_message(
**signature_kwargs,
signature_version=b'\x00',
version_specific_data=account.address,
)
signed = acct.signHash(hashed_msg, account.privateKey)
new_addr = acct.recoverHash(hashed_msg, signature=signed.signature)
assert new_addr == account.address


def test_eth_short_account_address_sign_data_with_intended_validator(acct, signature_kwargs):
account = acct.create()

address_in_bytes = to_bytes(hexstr=account.address)
# Test for all lengths of addresses < 20 bytes
for i in range(1, 21):
with pytest.raises(TypeError):
# Raise TypeError if the address is less than 20 bytes
defunct_hash_message(
**signature_kwargs,
signature_version=b'\x00',
version_specific_data=to_hex(address_in_bytes[:-i]),
)


def test_eth_long_account_address_sign_data_with_intended_validator(acct, signature_kwargs):
account = acct.create()

address_in_bytes = to_bytes(hexstr=account.address)
with pytest.raises(TypeError):
# Raise TypeError if the address is more than 20 bytes
defunct_hash_message(
**signature_kwargs,
signature_version=b'\x00',
version_specific_data=to_hex(address_in_bytes + b'\x00'),
)


@pytest.mark.parametrize(
'txn, private_key, expected_raw_tx, tx_hash, r, s, v',
(
Expand Down

0 comments on commit 7be70c3

Please sign in to comment.