From d158ec6ddf1baad20c699c5977055a81822abaae Mon Sep 17 00:00:00 2001 From: Steven Kryskalla Date: Wed, 2 Jun 2021 11:12:32 -0400 Subject: [PATCH] feat: add support for EIP-712 message signing This change adds a dependency for the `eip712` package and uses it to implement a new `LocalAccount.sign_message` method. --- CHANGELOG.md | 3 +++ brownie/network/account.py | 27 ++++++++++++++++++++++++++ docs/account-management.rst | 25 ++++++++++++++++++++++++ requirements.in | 1 + requirements.txt | 9 +++++++++ tests/network/account/test_accounts.py | 23 +++++++++++++++++++++- 6 files changed, 87 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4bd2ecb..12789310e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/eth-brownie/brownie) +### Added +- Added `LocalAccount.sign_message` method to sign `EIP712Message` objects ([#???](https://github.com/eth-brownie/brownie/pull/???)) + ## [1.14.6](https://github.com/eth-brownie/brownie/tree/v1.14.5) - 2021-04-20 ### Changed - Upgraded web3 dependency to version 5.18.0 ([#1064](https://github.com/eth-brownie/brownie/pull/1064)) diff --git a/brownie/network/account.py b/brownie/network/account.py index f15c8e71d..fec693feb 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -11,6 +11,9 @@ import eth_account import eth_keys import rlp +from eip712.messages import EIP712Message, _hash_eip191_message +from eth_account._utils.signing import sign_message_hash +from eth_account.datastructures import SignedMessage from eth_utils import keccak from hexbytes import HexBytes @@ -757,6 +760,30 @@ def save(self, filename: str, overwrite: bool = False) -> str: json.dump(encrypted, fp) return str(json_file) + def sign_message(self, message: EIP712Message) -> SignedMessage: + """Signs an `EIP712Message` using this account's private key. + + Args: + message: An `EIP712Message` instance. + + Returns: + An eth_account `SignedMessage` instance. + """ + # some of this code is from: + # https://github.com/ethereum/eth-account/blob/00e7b10/eth_account/account.py#L577 + # https://github.com/ethereum/eth-account/blob/00e7b10/eth_account/account.py#L502 + msg_hash_bytes = HexBytes(_hash_eip191_message(message.signable_message)) + assert len(msg_hash_bytes) == 32, "The message hash must be exactly 32-bytes" + eth_private_key = eth_keys.keys.PrivateKey(HexBytes(self.private_key)) + (v, r, s, eth_signature_bytes) = sign_message_hash(eth_private_key, msg_hash_bytes) + return SignedMessage( + messageHash=msg_hash_bytes, + r=r, + s=s, + v=v, + signature=HexBytes(eth_signature_bytes), + ) + def _transact(self, tx: Dict, allow_revert: bool) -> None: if allow_revert is None: allow_revert = bool(CONFIG.network_type == "development") diff --git a/docs/account-management.rst b/docs/account-management.rst index 7d6832a4f..2cbc51ab6 100644 --- a/docs/account-management.rst +++ b/docs/account-management.rst @@ -93,3 +93,28 @@ To do so, add the account to the ``unlock`` setting in a project's :ref:`configu The unlocked accounts are automatically added to the :func:`Accounts ` container. Note that you might need to fund the unlocked accounts manually. + +Signing Messages +================ + +To sign an `EIP712Message `_, use the :func:`LocalAccount.sign_message ` method to produce an ``eth_account`` `SignableMessage `_ object: + +.. code-block:: python + + >>> from eip712.messages import EIP712Message, EIP712Type + >>> local = accounts.add(private_key="0x416b8a7d9290502f5661da81f0cf43893e3d19cb9aea3c426cfb36e8186e9c09") + >>> class TestSubType(EIP712Type): + ... inner: "uint256" + ... + >>> class TestMessage(EIP712Message): + ... _name_: "string" = "Brownie Test Message" + ... outer: "uint256" + ... sub: TestSubType + ... + >>> msg = TestMessage(outer=1, sub=TestSubType(inner=2)) + >>> signed = local.sign_message(msg) + >>> type(signed) + + >>> signed.messageHash.hex() + '0x2a180b353c317ae903c063141592ec360b25be9f75c60ae16ca19f5578f70a50' + diff --git a/requirements.in b/requirements.in index eacdb0e35..9426b7db7 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,5 @@ black==20.8b1 +eip712<0.2.0 eth-abi<3 eth-account<1 eth-event>=1.2.1,<2 diff --git a/requirements.txt b/requirements.txt index 6c7b678ed..ef77d7764 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,9 +31,14 @@ cytoolz==0.11.0 # via # eth-keyfile # eth-utils +dataclassy==0.10.2 + # via eip712 +eip712==0.1.0 + # via -r requirements.in eth-abi==2.1.1 # via # -r requirements.in + # eip712 # eth-account # eth-event # web3 @@ -59,6 +64,7 @@ eth-rlp==0.2.1 # via eth-account eth-typing==2.2.2 # via + # eip712 # eth-abi # eth-keys # eth-utils @@ -66,6 +72,7 @@ eth-typing==2.2.2 eth-utils==1.10.0 # via # -r requirements.in + # eip712 # eth-abi # eth-account # eth-event @@ -80,6 +87,7 @@ execnet==1.8.0 hexbytes==0.2.1 # via # -r requirements.in + # eip712 # eth-account # eth-event # eth-rlp @@ -135,6 +143,7 @@ py==1.10.0 # pytest-forked pycryptodome==3.10.1 # via + # eip712 # eth-hash # eth-keyfile # vyper diff --git a/tests/network/account/test_accounts.py b/tests/network/account/test_accounts.py index 90f580254..1e2f8bbfe 100644 --- a/tests/network/account/test_accounts.py +++ b/tests/network/account/test_accounts.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 - import pytest +from eip712.messages import EIP712Message, EIP712Type +from eth_account.datastructures import SignedMessage from brownie.exceptions import UnknownAccount from brownie.network.account import LocalAccount @@ -131,3 +132,23 @@ def test_mnemonic_offset_multiple(accounts): "0x44302d4c1e535b4FB77bc390e3053586ecA411b0", "0x1F413d7E7B85E557D9997E6714479C7848A9Ea07", ] + + +def test_sign_message(accounts): + class TestSubType(EIP712Type): + inner: "uint256" # type: ignore # noqa: F821 + + class TestMessage(EIP712Message): + _name_: "string" = "Brownie Tests" # type: ignore # noqa: F821 + value: "uint256" # type: ignore # noqa: F821 + default_value: "address" = "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF" # type: ignore # noqa: F821,E501 + sub: TestSubType + + local = accounts.add(priv_key) + msg = TestMessage(value=1, sub=TestSubType(inner=2)) + signed = local.sign_message(msg) + assert isinstance(signed, SignedMessage) + assert ( + signed.messageHash.hex() + == "0x131c497d4b815213752a2a00564dcf667c3bf3f85a410ef8cb50050b51959c26" + )