Skip to content

Commit

Permalink
Use _vs signatures instead of separate v and s (#258)
Browse files Browse the repository at this point in the history
* Initial commit

* Add field to the unvetter test

* Fake provider for e2e schemas validation testing

* Message signing function

* Remove compute_v

* Formatting

* Remove test

* Compute _vs in pauser test

* Formatting

* Fix pauser test

* Test recovery

* Separate test for shcema consistency

* Pre commit ruff

* Fix the comments

* Refactor

* Fix lock file
  • Loading branch information
hweawer authored Aug 26, 2024
1 parent f3d4103 commit 9faeba1
Show file tree
Hide file tree
Showing 21 changed files with 1,031 additions and 633 deletions.
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.4
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
1,178 changes: 706 additions & 472 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ruff = "^0.4.4"
pytest = "^8.2.0"
pytest-cov = "^5.0.0"
pyright = "^1.1.362"
pre-commit = "^3.8.0"

[tool.pytest.ini_options]
pythonpath = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging

from blockchain.typings import Web3
from cryptography.verify_signature import compute_vs
from eth_typing import Hash32
from transport.msg_types.deposit import DepositMessage
from web3.contract.contract import ContractFunction
Expand All @@ -24,7 +23,7 @@ def __init__(self, w3: Web3):
def _prepare_signs_for_deposit(quorum: list[DepositMessage]) -> tuple[tuple[str, str], ...]:
sorted_messages = sorted(quorum, key=lambda msg: int(msg['guardianAddress'], 16))

return tuple((msg['signature']['r'], compute_vs(msg['signature']['v'], msg['signature']['s'])) for msg in sorted_messages)
return tuple((msg['signature']['r'], msg['signature']['_vs']) for msg in sorted_messages)

def prepare_and_send(
self,
Expand Down
5 changes: 3 additions & 2 deletions src/bots/depositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
from transport.msg_providers.kafka import KafkaMessageProvider
from transport.msg_providers.rabbit import MessageType, RabbitProvider
from transport.msg_storage import MessageStorage
from transport.msg_types.deposit import DepositMessage, DepositMessageSchema, get_deposit_messages_sign_filter
from transport.msg_types.common import get_messages_sign_filter
from transport.msg_types.deposit import DepositMessage, DepositMessageSchema
from transport.msg_types.ping import PingMessageSchema, to_check_sum_address
from transport.types import TransportType
from web3.types import BlockData
Expand Down Expand Up @@ -100,7 +101,7 @@ def __init__(
filters=[
message_metrics_filter,
to_check_sum_address,
get_deposit_messages_sign_filter(self.w3),
get_messages_sign_filter(self.w3),
],
)

Expand Down
10 changes: 5 additions & 5 deletions src/bots/pauser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import variables
from blockchain.executor import Executor
from blockchain.typings import Web3
from cryptography.verify_signature import compute_vs
from metrics.metrics import UNEXPECTED_EXCEPTIONS
from metrics.transport_message_metrics import message_metrics_filter
from schema import Or, Schema
from transport.msg_providers.kafka import KafkaMessageProvider
from transport.msg_providers.rabbit import MessageType, RabbitProvider
from transport.msg_storage import MessageStorage
from transport.msg_types.pause import PauseMessage, PauseMessageSchema, get_pause_messages_sign_filter
from transport.msg_types.common import get_messages_sign_filter
from transport.msg_types.pause import PauseMessage, PauseMessageSchema
from transport.msg_types.ping import PingMessageSchema, to_check_sum_address
from transport.types import TransportType
from web3.types import BlockData
Expand Down Expand Up @@ -64,7 +64,7 @@ def __init__(self, w3: Web3):
filters=[
message_metrics_filter,
to_check_sum_address,
get_pause_messages_sign_filter(self.w3),
get_messages_sign_filter(self.w3),
],
)

Expand Down Expand Up @@ -119,7 +119,7 @@ def _send_pause(self, message: PauseMessage):
return False

pause_tx = self.w3.lido.deposit_security_module.pause_deposits(
message['blockNumber'], module_id, (message['signature']['r'], compute_vs(message['signature']['v'], message['signature']['s']))
message['blockNumber'], module_id, (message['signature']['r'], message['signature']['_vs'])
)

if not self.w3.transaction.check(pause_tx):
Expand All @@ -136,7 +136,7 @@ def _send_pause_v2(self, message: PauseMessage):
return False

pause_tx = self.w3.lido.deposit_security_module.pause_deposits_v2(
message['blockNumber'], (message['signature']['r'], compute_vs(message['signature']['v'], message['signature']['s']))
message['blockNumber'], (message['signature']['r'], message['signature']['_vs'])
)

result = self.w3.transaction.send(pause_tx, False, 6)
Expand Down
8 changes: 4 additions & 4 deletions src/bots/unvetter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import variables
from blockchain.executor import Executor
from blockchain.typings import Web3
from cryptography.verify_signature import compute_vs
from metrics.metrics import UNEXPECTED_EXCEPTIONS
from metrics.transport_message_metrics import message_metrics_filter
from schema import Or, Schema
from transport.msg_providers.kafka import KafkaMessageProvider
from transport.msg_providers.rabbit import MessageType, RabbitProvider
from transport.msg_storage import MessageStorage
from transport.msg_types.common import get_messages_sign_filter
from transport.msg_types.ping import PingMessageSchema, to_check_sum_address
from transport.msg_types.unvet import UnvetMessage, UnvetMessageSchema, get_unvet_messages_sign_filter
from transport.msg_types.unvet import UnvetMessage, UnvetMessageSchema
from transport.types import TransportType
from utils.bytes import from_hex_string_to_bytes
from web3.types import BlockData
Expand Down Expand Up @@ -69,7 +69,7 @@ def prepare_transport_bus(self):
filters=[
message_metrics_filter,
to_check_sum_address,
get_unvet_messages_sign_filter(self.w3),
get_messages_sign_filter(self.w3),
],
)

Expand Down Expand Up @@ -130,7 +130,7 @@ def _send_unvet_message(self, message: UnvetMessage) -> bool:
message['nonce'],
from_hex_string_to_bytes(message['operatorIds']),
from_hex_string_to_bytes(message['vettedKeysByOperator']),
(message['signature']['r'], compute_vs(message['signature']['v'], message['signature']['s'])),
(message['signature']['r'], message['signature']['_vs']),
)

if not self.w3.transaction.check(unvet_tx):
Expand Down
40 changes: 31 additions & 9 deletions src/cryptography/verify_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,51 @@
from typing import Any, List, Tuple

from eth_account import Account
from eth_account.account import VRS
from web3 import Web3

logger = logging.getLogger(__name__)

V_OFFSET = 27


# Solidity function
#
# function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
# bytes32 s;
# uint8 v;
# assembly {
# s := and(vs, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
# v := add(shr(255, vs), 27)
# }
# return recover(hash, v, r, s);
# }
def recover_vs(vs: str) -> tuple[VRS, VRS]:
"""
Recovers v and s parameters of the signature from _vs field
"""
# cut 0x
_vs = int.from_bytes(bytearray.fromhex(vs[2:]))
s = _vs & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
v = (_vs >> 255) + V_OFFSET
return v, s


def compute_vs(v: int, s: str) -> str:
"""Returns aggregated _vs value."""
if v < 27:
if v in [0, 1]:
v += 27
else:
msg = 'Signature invalid v byte.'
logger.error({'msg': 'Signature invalid v byte.', 'data': str(v)})
raise ValueError(msg)

if v < V_OFFSET and v not in [0, 1]:
logger.error({'msg': 'Signature invalid v byte.', 'data': str(v)})
raise ValueError('Signature invalid v byte.')
if v < V_OFFSET:
v += V_OFFSET
_vs = bytearray.fromhex(s[2:])
if not v % 2:
_vs[0] |= 0x80

return '0x' + _vs.hex()


def verify_message_with_signature(data: List[Any], abi: List[str], address: str, vrs: Tuple[int, str, str]) -> bool:
def verify_message_with_signature(data: List[Any], abi: List[str], address: str, vrs: Tuple[VRS, VRS, VRS]) -> bool:
"""
Check that message was correctly signed by provided address holder.
"""
Expand Down
16 changes: 10 additions & 6 deletions src/transport/msg_types/base.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import re
from typing import TypedDict

from schema import And, Regex, Schema
from eth_account.account import VRS
from schema import And, Optional, Regex, Schema

HASH_REGREX = Regex('^0x[0-9,A-F]{64}$', flags=re.IGNORECASE)
ADDRESS_REGREX = Regex('^0x[0-9,A-F]{40}$', flags=re.IGNORECASE)
HEX_BYTES_REGREX = Regex('^0x[0-9,A-F]*$', flags=re.IGNORECASE)

# v and s to be removed in future in favor of short signatures
SignatureSchema = Schema(
{
'v': int,
Optional('v'): int,
'r': And(str, HASH_REGREX.validate),
's': And(str, HASH_REGREX.validate),
Optional('s'): And(str, HASH_REGREX.validate),
'_vs': And(str, HASH_REGREX.validate),
},
ignore_extra_keys=True,
)


class Signature(TypedDict):
v: int
r: str
s: str
v: VRS
r: VRS
s: VRS
_vs: str
102 changes: 102 additions & 0 deletions src/transport/msg_types/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import logging
from typing import Any, Callable, List

from blockchain.typings import Web3
from cryptography.verify_signature import recover_vs, verify_message_with_signature
from eth_account.account import VRS
from metrics.metrics import UNEXPECTED_EXCEPTIONS
from transport.msg_providers.rabbit import MessageType
from transport.msg_types.deposit import DepositMessage
from transport.msg_types.pause import PauseMessage
from transport.msg_types.unvet import UnvetMessage
from utils.bytes import from_hex_string_to_bytes

logger = logging.getLogger(__name__)


def get_messages_sign_filter(web3: Web3) -> Callable:
"""Returns filter that checks message validity"""

def check_messages(msg: DepositMessage | PauseMessage | UnvetMessage) -> bool:
v, r, s = _vrs(msg)
data, abi = _verification_data(web3, msg)

is_valid = verify_message_with_signature(
data=data,
abi=abi,
address=msg['guardianAddress'],
vrs=(v, r, s),
)

if not is_valid:
label_name = _select_label(msg)
logger.error({'msg': 'Message verification failed.', 'value': msg})
UNEXPECTED_EXCEPTIONS.labels(label_name).inc()

return is_valid

return check_messages


def _vrs(msg: DepositMessage | PauseMessage | UnvetMessage) -> tuple[VRS, VRS, VRS]:
vs = msg['signature']['_vs']
r = msg['signature']['r']
v, s = recover_vs(vs)
return v, r, s


def _select_label(msg: DepositMessage | PauseMessage | UnvetMessage) -> str:
t = msg['type']
if t == MessageType.PAUSE:
return 'pause_message_verification_failed'
elif t == MessageType.UNVET:
return 'unvet_message_verification_failed'
elif t == MessageType.DEPOSIT:
return 'deposit_message_verification_failed'
else:
raise ValueError('Unsupported message type')


def _verification_data(web3: Web3, msg: DepositMessage | PauseMessage | UnvetMessage) -> tuple[List[Any], List[str]]:
t = msg['type']
if t == MessageType.PAUSE:
prefix = web3.lido.deposit_security_module.get_pause_message_prefix()
return _verification_data_pause(prefix, msg)
elif t == MessageType.UNVET:
prefix = web3.lido.deposit_security_module.get_unvet_message_prefix()
return _verification_data_unvet(prefix, msg)
elif t == MessageType.DEPOSIT:
prefix = web3.lido.deposit_security_module.get_attest_message_prefix()
return _verification_data_deposit(prefix, msg)
else:
raise ValueError('Unsupported message type')


def _verification_data_deposit(prefix: bytes, msg: DepositMessage) -> tuple[List[Any], List[str]]:
data = [prefix, msg['blockNumber'], msg['blockHash'], msg['depositRoot'], msg['stakingModuleId'], msg['nonce']]
abi = ['bytes32', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256']
return data, abi


def _verification_data_pause(prefix: bytes, msg: PauseMessage) -> tuple[List[Any], List[str]]:
if msg.get('stakingModuleId', -1) != -1:
data = [prefix, msg['blockNumber'], msg['stakingModuleId']]
abi = ['bytes32', 'uint256', 'uint256']
else:
data = [prefix, msg['blockNumber']]
abi = ['bytes32', 'uint256']
return data, abi


def _verification_data_unvet(prefix: bytes, msg: UnvetMessage) -> tuple[List[Any], List[str]]:
data = [
prefix,
msg['blockNumber'],
msg['blockHash'],
msg['stakingModuleId'],
msg['nonce'],
from_hex_string_to_bytes(msg['operatorIds']),
from_hex_string_to_bytes(msg['vettedKeysByOperator']),
]
abi = ['bytes32', 'uint256', 'bytes32', 'uint256', 'uint256', 'bytes', 'bytes']
return data, abi
32 changes: 1 addition & 31 deletions src/transport/msg_types/deposit.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import logging
from typing import Callable, TypedDict
from typing import TypedDict

from blockchain.typings import Web3
from cryptography.verify_signature import verify_message_with_signature
from metrics.metrics import UNEXPECTED_EXCEPTIONS
from schema import And, Schema
from transport.msg_types.base import ADDRESS_REGREX, HASH_REGREX, Signature, SignatureSchema

logger = logging.getLogger(__name__)


"""
Deposit msg example
{
Expand Down Expand Up @@ -58,29 +54,3 @@ class DepositMessage(TypedDict):
signature: Signature
stakingModuleId: int
app: dict


def get_deposit_messages_sign_filter(web3: Web3) -> Callable:
"""Returns filter that checks message validity"""

def check_deposit_messages(msg: DepositMessage) -> bool:
deposit_prefix = web3.lido.deposit_security_module.get_attest_message_prefix()

verified = verify_message_with_signature(
data=[deposit_prefix, msg['blockNumber'], msg['blockHash'], msg['depositRoot'], msg['stakingModuleId'], msg['nonce']],
abi=['bytes32', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256'],
address=msg['guardianAddress'],
vrs=(
msg['signature']['v'],
msg['signature']['r'],
msg['signature']['s'],
),
)

if not verified:
logger.error({'msg': 'Message verification failed.', 'value': msg})
UNEXPECTED_EXCEPTIONS.labels('deposit_message_verification_failed').inc()

return verified

return check_deposit_messages
Loading

0 comments on commit 9faeba1

Please sign in to comment.