diff --git a/eth_account/__init__.py b/eth_account/__init__.py index 4b6aa0a8..7c64990f 100644 --- a/eth_account/__init__.py +++ b/eth_account/__init__.py @@ -1 +1,3 @@ -from eth_account.account import Account # noqa: F401 +from eth_account.account import ( # noqa: F401 + Account, +) diff --git a/eth_account/_utils/signing.py b/eth_account/_utils/signing.py index 4fda3c71..2a80867a 100644 --- a/eth_account/_utils/signing.py +++ b/eth_account/_utils/signing.py @@ -1,4 +1,5 @@ from cytoolz import ( + curry, pipe, ) from eth_utils import ( @@ -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 @@ -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): diff --git a/eth_account/messages.py b/eth_account/messages.py index 7b37b9aa..749ee004 100644 --- a/eth_account/messages.py +++ b/eth_account/messages.py @@ -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() `. 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() `. + 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() `. Supply exactly one of the three arguments: @@ -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 @@ -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) diff --git a/tests/core/test_accounts.py b/tests/core/test_accounts.py index b40a66c0..01f3b296 100644 --- a/tests/core/test_accounts.py +++ b/tests/core/test_accounts.py @@ -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' @@ -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', (