diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 692877c20..763aac655 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -192,8 +192,12 @@ def sign(self, private_key: PrivateKey) -> Transaction: # We initialize the signature map for this body_bytes if it doesn't exist yet self._signature_map.setdefault(body_bytes, basic_types_pb2.SignatureMap()) - # Append the signature pair to the signature map for this transaction body - self._signature_map[body_bytes].sigPair.append(sig_pair) + # deduplication check + already_signed = any(sp.pubKeyPrefix == public_key_bytes for sp in self._signature_map[body_bytes].sigPair) + + # append only if not already signed + if not already_signed: + self._signature_map[body_bytes].sigPair.append(sig_pair) return self diff --git a/tests/unit/transaction_test.py b/tests/unit/transaction_test.py index efe8cbe84..5821d9e7a 100644 --- a/tests/unit/transaction_test.py +++ b/tests/unit/transaction_test.py @@ -3,6 +3,7 @@ import pytest from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.hapi.services import ( @@ -14,6 +15,9 @@ transaction_response_pb2, ) from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction +from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt from hiero_sdk_python.transaction.transaction_response import TransactionResponse from tests.unit.mock_server import mock_hedera_servers @@ -117,3 +121,42 @@ def test_execute_returns_receipt_without_error_when_validation_disabled(): receipt = tx.execute(client) assert receipt.status == ResponseCode.INVALID_SIGNATURE + + +def test_duplicate_signature_not_added(): + tx = TokenMintTransaction() + tx.set_transaction_id(TransactionId.generate(AccountId(0, 0, 1234))) + tx.set_node_account_id(AccountId(0, 0, 3)) + tx.set_token_id(TokenId(0, 0, 1)) + tx.set_amount(100) + key = PrivateKey.generate_ed25519() + tx.freeze() + tx.sign(key) + tx.sign(key) + assert tx._signature_map, "signature_map should not be empty after freeze+sign" # ← ADD HERE + body_bytes = next(iter(tx._signature_map.keys())) + sig_pairs = tx._signature_map[body_bytes].sigPair + assert len(sig_pairs) == 1, "Expected 1 signature for duplicate key" + + +def test_multiple_keys_still_work(): + tx = TokenMintTransaction() + tx.set_transaction_id(TransactionId.generate(AccountId(0, 0, 1234))) + tx.set_node_account_id(AccountId(0, 0, 3)) + tx.set_token_id(TokenId(0, 0, 1)) + tx.set_amount(100) + key1 = PrivateKey.generate_ed25519() + key2 = PrivateKey.generate_ed25519() + tx.freeze() + tx.sign(key1) + tx.sign(key2) + assert tx._signature_map, "signature_map should not be empty after freeze+sign" + body_bytes = next(iter(tx._signature_map.keys())) + sig_pairs = tx._signature_map[body_bytes].sigPair + assert len(sig_pairs) == 2, "Expected 2 signatures for different keys" + pubkey_prefixes = {sp.pubKeyPrefix for sp in sig_pairs} + expected_prefixes = { + key1.public_key().to_bytes_raw(), + key2.public_key().to_bytes_raw(), + } + assert pubkey_prefixes == expected_prefixes, "Signatures should match key1 and key2 exactly"