From cf7227fdc0563e25853d555a928f425de1dd3715 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Thu, 9 Jan 2025 14:53:50 +0200 Subject: [PATCH] raise error if options are not set for hash signing --- multiversx_sdk/accounts/ledger_account.py | 14 +++-- multiversx_sdk/core/transaction_computer.py | 61 ++++++++++++++++----- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/multiversx_sdk/accounts/ledger_account.py b/multiversx_sdk/accounts/ledger_account.py index df203675..d6408109 100644 --- a/multiversx_sdk/accounts/ledger_account.py +++ b/multiversx_sdk/accounts/ledger_account.py @@ -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) diff --git a/multiversx_sdk/core/transaction_computer.py b/multiversx_sdk/core/transaction_computer.py index 555f059a..a9d97fae 100644 --- a/multiversx_sdk/core/transaction_computer.py +++ b/multiversx_sdk/core/transaction_computer.py @@ -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 @@ -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) @@ -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 @@ -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() @@ -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: @@ -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 @@ -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 @@ -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")