Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AccountAPI): sign raw hash32 #1966

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from eip712.messages import SignableMessage as EIP712SignableMessage
from eth_account import Account
from eth_account.messages import encode_defunct
from hexbytes import HexBytes
from eth_pydantic_types import HexBytes

from ape.api.address import BaseAddress
from ape.api.transactions import ReceiptAPI, TransactionAPI
Expand Down Expand Up @@ -57,6 +57,23 @@ def alias(self) -> Optional[str]:
"""
return None

def sign_raw_msghash(self, msghash: HexBytes) -> Optional[MessageSignature]:
"""
Sign a raw message hash.

Args:
msghash (:class:`~eth_pydantic_types.HexBytes`):
The message hash to sign. Plugins may or may not support this operation.
Default implementation is to raise ``NotImplementedError``.

Returns:
:class:`~ape.types.signatures.MessageSignature` (optional):
The signature corresponding to the message.
"""
raise NotImplementedError(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The @raises_not_implemented decorator does exactly this! same messaging.

f"Raw message signing is not supported by '{self.__class__.__name__}'"
)

@abstractmethod
def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]:
"""
Expand Down Expand Up @@ -295,8 +312,9 @@ def declare(self, contract: "ContractContainer", *args, **kwargs) -> ReceiptAPI:

def check_signature(
self,
data: Union[SignableMessage, TransactionAPI, str, EIP712Message, int],
data: Union[SignableMessage, TransactionAPI, str, EIP712Message, int, bytes],
signature: Optional[MessageSignature] = None, # TransactionAPI doesn't need it
recover_using_eip191: bool = True,
) -> bool:
"""
Verify a message or transaction was signed by this account.
Expand All @@ -307,6 +325,10 @@ def check_signature(
signature (Optional[:class:`~ape.types.signatures.MessageSignature`]):
The signature to check. Defaults to ``None`` and is not needed when the first
argument is a transaction class.
recover_using_eip191 (bool):
Perform recovery using EIP-191 signed message check. If set False, then will attempt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Perform recovery using EIP-191 signed message check. If set False, then will attempt
Perform recovery using EIP-191 signed message check. If False, then will attempt

recovery as raw hash. `data`` must be a 32 byte hash if this is set False.
Defaults to ``True``.

Returns:
bool: ``True`` if the data was signed by this account. ``False`` otherwise.
Expand All @@ -315,6 +337,8 @@ def check_signature(
data = encode_defunct(text=data)
elif isinstance(data, int):
data = encode_defunct(hexstr=HexBytes(data).hex())
elif isinstance(data, bytes) and (len(data) != 32 or recover_using_eip191):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small perf, maybe, for short-circuit eval on the or stmt

Suggested change
elif isinstance(data, bytes) and (len(data) != 32 or recover_using_eip191):
elif isinstance(data, bytes) and (recover_using_eip191 or len(data) != 32):

as checking a bool is prolly faster than a length check

data = encode_defunct(data)
elif isinstance(data, EIP712Message):
data = data.signable_message
if isinstance(data, (SignableMessage, EIP712SignableMessage)):
Expand All @@ -329,6 +353,9 @@ def check_signature(
elif isinstance(data, TransactionAPI):
return self.address == Account.recover_transaction(data.serialize_transaction())

elif isinstance(data, bytes) and len(data) == 32 and not recover_using_eip191:
return self.address == Account._recover_hash(data, vrs=signature)

else:
raise AccountsError(f"Unsupported message type: {type(data)}.")

Expand Down
2 changes: 1 addition & 1 deletion src/ape/types/signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from eth_account import Account
from eth_account.messages import SignableMessage
from eth_pydantic_types import HexBytes
from eth_utils import to_bytes, to_hex
from hexbytes import HexBytes
from pydantic.dataclasses import dataclass

try:
Expand Down
39 changes: 31 additions & 8 deletions src/ape_accounts/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,17 @@ def delete(self):

def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]:
if isinstance(msg, str):
user_approves = self.__autosign or click.confirm(f"Message: {msg}\n\nSign: ")
display_msg = f"Signing raw string: '{msg}'"
msg = encode_defunct(text=msg)

elif isinstance(msg, int):
user_approves = self.__autosign or click.confirm(f"Message: {msg}\n\nSign: ")
display_msg = f"Signing raw integer: {msg}"
msg = encode_defunct(hexstr=HexBytes(msg).hex())

elif isinstance(msg, bytes):
user_approves = self.__autosign or click.confirm(f"Message: {msg.hex()}\n\nSign: ")
display_msg = f"Signing raw bytes: '{msg.hex()}'"
msg = encode_defunct(primitive=msg)

elif isinstance(msg, EIP712Message):
# Display message data to user
display_msg = "Signing EIP712 Message\n"
Expand All @@ -193,20 +196,22 @@ def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]
for field, value in msg._body_["message"].items():
display_msg += f"\t{field}: {value}\n"

user_approves = self.__autosign or click.confirm(f"{display_msg}\nSign: ")

# Convert EIP712Message to SignableMessage for handling below
msg = msg.signable_message

elif isinstance(msg, SignableMessage):
user_approves = self.__autosign or click.confirm(f"{msg}\n\nSign: ")
display_msg = str(msg)

else:
logger.warning("Unsupported message type, (type=%r, msg=%r)", type(msg), msg)
return None

if not user_approves:
if self.__autosign or click.confirm(f"{display_msg}\n\nSign: "):
signed_msg = EthAccount.sign_message(msg, self.__key)

else:
return None

signed_msg = EthAccount.sign_message(msg, self.__key)
return MessageSignature(
v=signed_msg.v,
r=to_bytes(signed_msg.r),
Expand All @@ -229,6 +234,24 @@ def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[Tr

return txn

def sign_raw_msghash(self, msghash: HexBytes) -> Optional[MessageSignature]:
logger.warning(
"Signing a raw hash directly is a dangerous action which could risk "
"substantial losses! Only confirm if you are 100% sure of the origin!"
)

# NOTE: Signing a raw hash is so dangerous, we don't want to allow autosigning it
if not click.confirm("Please confirm you wish to sign using `EthAccount.signHash`"):
return None

signed_msg = EthAccount.signHash(msghash, self.__key)

return MessageSignature(
v=signed_msg.v,
r=to_bytes(signed_msg.r),
s=to_bytes(signed_msg.s),
)

def set_autosign(self, enabled: bool, passphrase: Optional[str] = None):
"""
Allow this account to automatically sign messages and transactions.
Expand Down
11 changes: 10 additions & 1 deletion src/ape_test/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from eip712.messages import EIP712Message
from eth_account import Account as EthAccount
from eth_account.messages import SignableMessage, encode_defunct
from eth_pydantic_types import HexBytes
from eth_utils import to_bytes
from hexbytes import HexBytes

from ape.api import TestAccountAPI, TestAccountContainerAPI, TransactionAPI
from ape.exceptions import SignatureError
Expand Down Expand Up @@ -142,3 +142,12 @@ def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[Tr
)

return txn

def sign_raw_msghash(self, msghash: HexBytes) -> MessageSignature:
signed_msg = EthAccount.signHash(msghash, self.private_key)

return MessageSignature(
v=signed_msg.v,
r=to_bytes(signed_msg.r),
s=to_bytes(signed_msg.s),
)
15 changes: 15 additions & 0 deletions tests/functional/test_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ def test_sign_message_with_prompts(runner, keyfile_account, message):
assert start_nonce == end_nonce


def test_sign_raw_hash(runner, keyfile_account):
# NOTE: `message` is a 32 byte raw hash, which is treated specially
message = b"\xAB" * 32

# "y\na\ny": yes sign raw hash, password, yes keep unlocked
with runner.isolation(input=f"y\n{PASSPHRASE}\ny"):
signature = keyfile_account.sign_raw_msghash(message)
assert keyfile_account.check_signature(message, signature, recover_using_eip191=False)

# "n\nn": no sign raw hash: don't sign
with runner.isolation(input="n"):
signature = keyfile_account.sign_message(message)
assert signature is None


def test_transfer(sender, receiver, eth_tester_provider, convert):
initial_receiver_balance = receiver.balance
initial_sender_balance = sender.balance
Expand Down
Loading