diff --git a/crypto/identity/private_key.py b/crypto/identity/private_key.py index d85703b..b5d307a 100644 --- a/crypto/identity/private_key.py +++ b/crypto/identity/private_key.py @@ -27,6 +27,22 @@ def sign(self, message: bytes) -> bytes: return bytes([der[64]]) + der[0:64] + def sign_to_ecdsa(self, message: bytes) -> bytes: + """Sign a message with this private key object in ECDSA format + + Args: + message (bytes): bytes data you want to sign + + Returns: + bytes: signature of the signed message + """ + + message_hash = bytes.fromhex(keccak.new(data=message, digest_bits=256).hexdigest()) + + der = self.private_key.sign_recoverable(message_hash, hasher=None) + + return der[0:64] + bytes([der[64]]) + def to_hex(self): """Returns a private key in hex format diff --git a/crypto/transactions/builder/abstract_transaction_builder.py b/crypto/transactions/builder/abstract_transaction_builder.py index a90d2a7..2a65e49 100644 --- a/crypto/transactions/builder/abstract_transaction_builder.py +++ b/crypto/transactions/builder/abstract_transaction_builder.py @@ -47,6 +47,13 @@ def sign(self, passphrase: str): self.transaction.data['hash'] = self.transaction.get_id() return self + def legacy_second_sign(self, passphrase: str, second_passphrase: str): + self.sign(passphrase) + + self.transaction.legacy_second_sign(PrivateKey.from_passphrase(second_passphrase)) + + return self + def verify(self): return self.transaction.verify() diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py index d62a2eb..8fd0109 100644 --- a/crypto/transactions/types/abstract_transaction.py +++ b/crypto/transactions/types/abstract_transaction.py @@ -35,6 +35,15 @@ def sign(self, private_key: PrivateKey): return self + def legacy_second_sign(self, second_private_key: PrivateKey): + transaction_hash = TransactionUtils.to_buffer(self.data, skip_signature=True).decode() + + message = bytes.fromhex(transaction_hash) + + self.data['legacySecondSignature'] = second_private_key.sign_to_ecdsa(message).hex() + + return self + def recover_sender(self): signature_with_recid = self._get_signature() if not signature_with_recid: diff --git a/crypto/utils/transaction_utils.py b/crypto/utils/transaction_utils.py index 2049c60..6314457 100644 --- a/crypto/utils/transaction_utils.py +++ b/crypto/utils/transaction_utils.py @@ -30,19 +30,18 @@ def to_buffer(cls, transaction: dict, skip_signature: bool = False) -> bytes: fields.append(cls.to_be_array(int(transaction['v']) + (Network.get_network().chain_id() * 2 + 35))) fields.append(bytes.fromhex(transaction['r'])) fields.append(bytes.fromhex(transaction['s'])) + + if 'legacySecondSignature' in transaction and transaction['legacySecondSignature']: + fields.append(bytes.fromhex(transaction['legacySecondSignature'])) else: # Push chainId + 0s for r and s fields.append(cls.to_be_array(Network.get_network().chain_id())) fields.append(cls.to_be_array(0)) fields.append(cls.to_be_array(0)) - # TODO: second signature handling - encoded = RlpEncoder.encode(fields) - hash_input = encoded - - return hash_input.encode() + return encoded.encode() @classmethod def to_hash(cls, transaction: dict, skip_signature: bool = False) -> str: diff --git a/tests/fixtures/transactions/transfer-legacy-second-signature.json b/tests/fixtures/transactions/transfer-legacy-second-signature.json new file mode 100644 index 0000000..b803943 --- /dev/null +++ b/tests/fixtures/transactions/transfer-legacy-second-signature.json @@ -0,0 +1,19 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "21000", + "to": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "value": "100000000", + "data": "", + "network": 11812, + "v": 0, + "r": "a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9", + "s": "2d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79", + "legacySecondSignature": "094b33b2d10c4d48cff9a9b10f79990c9a902a762b00d2f2a4d2ddad7823b59332b2da193265068c67cefea9ca8bc84e47acec406f3300a0a10b77a56ea8c18801", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "a39435ec5de418e77479856d06a653efc171afe43e091472af22ee359eeb83be" + }, + "serialized": "f8ad0185012a05f200825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080825c6ba0a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9a02d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79b841094b33b2d10c4d48cff9a9b10f79990c9a902a762b00d2f2a4d2ddad7823b59332b2da193265068c67cefea9ca8bc84e47acec406f3300a0a10b77a56ea8c18801" +} diff --git a/tests/transactions/builder/conftest.py b/tests/transactions/builder/conftest.py index 7e43b9a..4da1f36 100644 --- a/tests/transactions/builder/conftest.py +++ b/tests/transactions/builder/conftest.py @@ -6,6 +6,12 @@ def passphrase(): return 'found lobster oblige describe ready addict body brave live vacuum display salute lizard combine gift resemble race senior quality reunion proud tell adjust angle' +@pytest.fixture +def second_passphrase(): + """Second Passphrase used for tests""" + + return 'gold favorite math anchor detect march purpose such sausage crucial reform novel connect misery update episode invite salute barely garbage exclude winner visa cruise' + @pytest.fixture def validator_public_key(): """BLS Public used for validator tests""" diff --git a/tests/transactions/builder/test_transfer_builder.py b/tests/transactions/builder/test_transfer_builder.py index 5f9db21..8213f1a 100644 --- a/tests/transactions/builder/test_transfer_builder.py +++ b/tests/transactions/builder/test_transfer_builder.py @@ -28,6 +28,34 @@ def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixtur assert builder.transaction.data['hash'] == fixture['data']['hash'] assert builder.verify() +def test_it_should_sign_with_a_legacy_second_signature(passphrase, second_passphrase, load_transaction_fixture): + fixture = load_transaction_fixture('transactions/transfer-legacy-second-signature') + + builder = ( + TransferBuilder + .new() + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .gas_limit(fixture['data']['gasLimit']) + .to(fixture['data']['to']) + .value(fixture['data']['value']) + .legacy_second_sign(passphrase, second_passphrase) + ) + + assert builder.transaction.data['gasPrice'] == int(fixture['data']['gasPrice']) + assert builder.transaction.data['gasLimit'] == int(fixture['data']['gasLimit']) + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['to'] == fixture['data']['to'] + assert builder.transaction.data['value'] == int(fixture['data']['value']) + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + assert builder.transaction.data['legacySecondSignature'] == fixture['data']['legacySecondSignature'] + + assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.data['hash'] == fixture['data']['hash'] + assert builder.verify() + def test_it_should_handle_unit_converter(passphrase, address): builder = ( TransferBuilder