Skip to content

Commit

Permalink
feat: add support for EIP-712 message signing
Browse files Browse the repository at this point in the history
This change adds a dependency for the `eip712` package and uses it to
implement a new `LocalAccount.sign_message` method.
  • Loading branch information
lost-theory committed Jun 2, 2021
1 parent aaffc21 commit ab4ad0b
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([#1097](https://github.com/eth-brownie/brownie/pull/1097))

## [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))
Expand Down
27 changes: 27 additions & 0 deletions brownie/network/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
25 changes: 25 additions & 0 deletions docs/account-management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <brownie.network.account.Accounts>` container.
Note that you might need to fund the unlocked accounts manually.
Signing Messages
================
To sign an `EIP712Message <https://pypi.org/project/eip712/>`_, use the :func:`LocalAccount.sign_message <brownie.network.account.LocalAccount.sign_message>` method to produce an ``eth_account`` `SignableMessage <https://eth-account.readthedocs.io/en/stable/eth_account.html#eth_account.messages.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)
<class 'eth_account.datastructures.SignedMessage'>
>>> signed.messageHash.hex()
'0x2a180b353c317ae903c063141592ec360b25be9f75c60ae16ca19f5578f70a50'
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
black==20.8b1
eip712<0.2.0
eth-abi<3
eth-account<1
eth-event>=1.2.1,<2
Expand Down
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -59,13 +64,15 @@ eth-rlp==0.2.1
# via eth-account
eth-typing==2.2.2
# via
# eip712
# eth-abi
# eth-keys
# eth-utils
# web3
eth-utils==1.10.0
# via
# -r requirements.in
# eip712
# eth-abi
# eth-account
# eth-event
Expand All @@ -80,6 +87,7 @@ execnet==1.8.0
hexbytes==0.2.1
# via
# -r requirements.in
# eip712
# eth-account
# eth-event
# eth-rlp
Expand Down Expand Up @@ -135,6 +143,7 @@ py==1.10.0
# pytest-forked
pycryptodome==3.10.1
# via
# eip712
# eth-hash
# eth-keyfile
# vyper
Expand Down
23 changes: 22 additions & 1 deletion tests/network/account/test_accounts.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
)

0 comments on commit ab4ad0b

Please sign in to comment.