Skip to content

Commit

Permalink
raise error if options are not set for hash signing
Browse files Browse the repository at this point in the history
  • Loading branch information
popenta committed Jan 9, 2025
1 parent 1b494c7 commit cf7227f
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 20 deletions.
14 changes: 9 additions & 5 deletions multiversx_sdk/accounts/ledger_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ def set_address(self, address_index: int = 0):
app.close()

def sign_transaction(self, transaction: Transaction) -> bytes:
"""Sets `version` and `options` fields to sign transaction by hash."""
transaction_computer = TransactionComputer()

if not transaction_computer.has_options_set_for_hash_signing(transaction):
raise Exception(
"Invalid transaction options. Set the least significant bit of the `options` property to `1`."
)

app = LedgerApp()
app.set_address(self.address_index)

transaction_computer = TransactionComputer()
transaction_computer.apply_options_for_hash_signing(transaction)
serialized_transaction = transaction_computer.compute_bytes_for_signing(transaction)
signature = app.sign_transaction(serialized_transaction)
hash = transaction_computer.compute_hash_for_signing(transaction)
signature = app.sign_transaction(hash)

app.close()
return bytes.fromhex(signature)
Expand Down
61 changes: 46 additions & 15 deletions multiversx_sdk/core/transaction_computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

from multiversx_sdk.core.address import Address
from multiversx_sdk.core.constants import (
BECH32_ADDRESS_LENGTH, DIGEST_SIZE,
BECH32_ADDRESS_LENGTH,
DIGEST_SIZE,
MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS,
TRANSACTION_OPTIONS_TX_GUARDED, TRANSACTION_OPTIONS_TX_HASH_SIGN)
TRANSACTION_OPTIONS_TX_GUARDED,
TRANSACTION_OPTIONS_TX_HASH_SIGN,
)
from multiversx_sdk.core.errors import BadUsageError, NotEnoughGasError
from multiversx_sdk.core.interfaces import INetworkConfig
from multiversx_sdk.core.proto.transaction_serializer import ProtoSerializer
Expand All @@ -21,9 +24,13 @@ class TransactionComputer:
def __init__(self) -> None:
pass

def compute_transaction_fee(self, transaction: Transaction, network_config: INetworkConfig) -> int:
def compute_transaction_fee(
self, transaction: Transaction, network_config: INetworkConfig
) -> int:
"""`TransactionsFactoryConfig` can be used here as the `network_config`."""
move_balance_gas = network_config.min_gas_limit + len(transaction.data) * network_config.gas_per_data_byte
move_balance_gas = (
network_config.min_gas_limit + len(transaction.data) * network_config.gas_per_data_byte
)
if move_balance_gas > transaction.gas_limit:
raise NotEnoughGasError(transaction.gas_limit)

Expand All @@ -40,6 +47,9 @@ def compute_transaction_fee(self, transaction: Transaction, network_config: INet
def compute_bytes_for_signing(self, transaction: Transaction) -> bytes:
self._ensure_fields(transaction)

if self.has_options_set_for_hash_signing(transaction):
return self.compute_hash_for_signing(transaction)

dictionary = self._to_dictionary(transaction)
serialized = self._dict_to_json(dictionary)
return serialized
Expand All @@ -53,7 +63,12 @@ def compute_bytes_for_verifying(self, transaction: Transaction) -> bytes:
return self.compute_bytes_for_signing(transaction)

def compute_hash_for_signing(self, transaction: Transaction) -> bytes:
return keccak.new(digest_bits=256).update(self.compute_bytes_for_signing(transaction)).digest()
self._ensure_fields(transaction)

dictionary = self._to_dictionary(transaction)
serialized = self._dict_to_json(dictionary)

return keccak.new(digest_bits=256).update(serialized).digest()

def compute_transaction_hash(self, transaction: Transaction) -> bytes:
proto = ProtoSerializer()
Expand All @@ -62,10 +77,14 @@ def compute_transaction_hash(self, transaction: Transaction) -> bytes:
return bytes.fromhex(tx_hash)

def has_options_set_for_guarded_transaction(self, transaction: Transaction) -> bool:
return (transaction.options & TRANSACTION_OPTIONS_TX_GUARDED) == TRANSACTION_OPTIONS_TX_GUARDED
return (
transaction.options & TRANSACTION_OPTIONS_TX_GUARDED
) == TRANSACTION_OPTIONS_TX_GUARDED

def has_options_set_for_hash_signing(self, transaction: Transaction) -> bool:
return (transaction.options & TRANSACTION_OPTIONS_TX_HASH_SIGN) == TRANSACTION_OPTIONS_TX_HASH_SIGN
return (
transaction.options & TRANSACTION_OPTIONS_TX_HASH_SIGN
) == TRANSACTION_OPTIONS_TX_HASH_SIGN

def apply_guardian(self, transaction: Transaction, guardian: Address) -> None:
if transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS:
Expand All @@ -87,19 +106,29 @@ def is_relayed_v3_transaction(self, transaction: Transaction) -> bool:

def _ensure_fields(self, transaction: Transaction) -> None:
if len(transaction.sender.to_bech32()) != BECH32_ADDRESS_LENGTH:
raise BadUsageError("Invalid `sender` field. Should be the bech32 address of the sender.")
raise BadUsageError(
"Invalid `sender` field. Should be the bech32 address of the sender."
)

if len(transaction.receiver.to_bech32()) != BECH32_ADDRESS_LENGTH:
raise BadUsageError("Invalid `receiver` field. Should be the bech32 address of the receiver.")
raise BadUsageError(
"Invalid `receiver` field. Should be the bech32 address of the receiver."
)

if not len(transaction.chain_id):
raise BadUsageError("The `chainID` field is not set")

if transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS:
if self.has_options_set_for_guarded_transaction(transaction) or self.has_options_set_for_hash_signing(transaction):
raise BadUsageError(f"Non-empty transaction options requires transaction version >= {MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS}")

def _to_dictionary(self, transaction: Transaction, with_signature: bool = False) -> dict[str, Any]:
if self.has_options_set_for_guarded_transaction(
transaction
) or self.has_options_set_for_hash_signing(transaction):
raise BadUsageError(
f"Non-empty transaction options requires transaction version >= {MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS}"
)

def _to_dictionary(
self, transaction: Transaction, with_signature: bool = False
) -> dict[str, Any]:
"""Only used when serializing transaction for signing. Internal use only."""
dictionary: dict[str, Any] = OrderedDict()
dictionary["nonce"] = transaction.nonce
Expand All @@ -112,7 +141,9 @@ def _to_dictionary(self, transaction: Transaction, with_signature: bool = False)
dictionary["senderUsername"] = b64encode(transaction.sender_username.encode()).decode()

if transaction.receiver_username:
dictionary["receiverUsername"] = b64encode(transaction.receiver_username.encode()).decode()
dictionary["receiverUsername"] = b64encode(
transaction.receiver_username.encode()
).decode()

dictionary["gasPrice"] = transaction.gas_price
dictionary["gasLimit"] = transaction.gas_limit
Expand Down Expand Up @@ -141,4 +172,4 @@ def _to_dictionary(self, transaction: Transaction, with_signature: bool = False)
return dictionary

def _dict_to_json(self, dictionary: dict[str, Any]) -> bytes:
return json.dumps(dictionary, separators=(',', ':')).encode("utf-8")
return json.dumps(dictionary, separators=(",", ":")).encode("utf-8")

0 comments on commit cf7227f

Please sign in to comment.