Skip to content

Commit 5a9fd5a

Browse files
authored
refactor: legacy second signature support (#175)
1 parent aeddb22 commit 5a9fd5a

File tree

7 files changed

+89
-5
lines changed

7 files changed

+89
-5
lines changed

crypto/identity/private_key.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ def sign(self, message: bytes) -> bytes:
2727

2828
return bytes([der[64]]) + der[0:64]
2929

30+
def sign_to_ecdsa(self, message: bytes) -> bytes:
31+
"""Sign a message with this private key object in ECDSA format
32+
33+
Args:
34+
message (bytes): bytes data you want to sign
35+
36+
Returns:
37+
bytes: signature of the signed message
38+
"""
39+
40+
message_hash = bytes.fromhex(keccak.new(data=message, digest_bits=256).hexdigest())
41+
42+
der = self.private_key.sign_recoverable(message_hash, hasher=None)
43+
44+
return der[0:64] + bytes([der[64]])
45+
3046
def to_hex(self):
3147
"""Returns a private key in hex format
3248

crypto/transactions/builder/abstract_transaction_builder.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ def sign(self, passphrase: str):
4747
self.transaction.data['hash'] = self.transaction.get_id()
4848
return self
4949

50+
def legacy_second_sign(self, passphrase: str, second_passphrase: str):
51+
self.sign(passphrase)
52+
53+
self.transaction.legacy_second_sign(PrivateKey.from_passphrase(second_passphrase))
54+
55+
return self
56+
5057
def verify(self):
5158
return self.transaction.verify()
5259

crypto/transactions/types/abstract_transaction.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ def sign(self, private_key: PrivateKey):
3535

3636
return self
3737

38+
def legacy_second_sign(self, second_private_key: PrivateKey):
39+
transaction_hash = TransactionUtils.to_buffer(self.data, skip_signature=True).decode()
40+
41+
message = bytes.fromhex(transaction_hash)
42+
43+
self.data['legacySecondSignature'] = second_private_key.sign_to_ecdsa(message).hex()
44+
45+
return self
46+
3847
def recover_sender(self):
3948
signature_with_recid = self._get_signature()
4049
if not signature_with_recid:

crypto/utils/transaction_utils.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,18 @@ def to_buffer(cls, transaction: dict, skip_signature: bool = False) -> bytes:
3030
fields.append(cls.to_be_array(int(transaction['v']) + (Network.get_network().chain_id() * 2 + 35)))
3131
fields.append(bytes.fromhex(transaction['r']))
3232
fields.append(bytes.fromhex(transaction['s']))
33+
34+
if 'legacySecondSignature' in transaction and transaction['legacySecondSignature']:
35+
fields.append(bytes.fromhex(transaction['legacySecondSignature']))
3336
else:
3437
# Push chainId + 0s for r and s
3538
fields.append(cls.to_be_array(Network.get_network().chain_id()))
3639
fields.append(cls.to_be_array(0))
3740
fields.append(cls.to_be_array(0))
3841

39-
# TODO: second signature handling
40-
4142
encoded = RlpEncoder.encode(fields)
4243

43-
hash_input = encoded
44-
45-
return hash_input.encode()
44+
return encoded.encode()
4645

4746
@classmethod
4847
def to_hash(cls, transaction: dict, skip_signature: bool = False) -> str:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"data": {
3+
"nonce": "1",
4+
"gasPrice": "5000000000",
5+
"gasLimit": "21000",
6+
"to": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22",
7+
"value": "100000000",
8+
"data": "",
9+
"network": 11812,
10+
"v": 0,
11+
"r": "a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9",
12+
"s": "2d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79",
13+
"legacySecondSignature": "094b33b2d10c4d48cff9a9b10f79990c9a902a762b00d2f2a4d2ddad7823b59332b2da193265068c67cefea9ca8bc84e47acec406f3300a0a10b77a56ea8c18801",
14+
"senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d",
15+
"from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd",
16+
"hash": "a39435ec5de418e77479856d06a653efc171afe43e091472af22ee359eeb83be"
17+
},
18+
"serialized": "f8ad0185012a05f200825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080825c6ba0a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9a02d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79b841094b33b2d10c4d48cff9a9b10f79990c9a902a762b00d2f2a4d2ddad7823b59332b2da193265068c67cefea9ca8bc84e47acec406f3300a0a10b77a56ea8c18801"
19+
}

tests/transactions/builder/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ def passphrase():
66

77
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'
88

9+
@pytest.fixture
10+
def second_passphrase():
11+
"""Second Passphrase used for tests"""
12+
13+
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'
14+
915
@pytest.fixture
1016
def validator_public_key():
1117
"""BLS Public used for validator tests"""

tests/transactions/builder/test_transfer_builder.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,34 @@ def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixtur
2828
assert builder.transaction.data['hash'] == fixture['data']['hash']
2929
assert builder.verify()
3030

31+
def test_it_should_sign_with_a_legacy_second_signature(passphrase, second_passphrase, load_transaction_fixture):
32+
fixture = load_transaction_fixture('transactions/transfer-legacy-second-signature')
33+
34+
builder = (
35+
TransferBuilder
36+
.new()
37+
.gas_price(fixture['data']['gasPrice'])
38+
.nonce(fixture['data']['nonce'])
39+
.gas_limit(fixture['data']['gasLimit'])
40+
.to(fixture['data']['to'])
41+
.value(fixture['data']['value'])
42+
.legacy_second_sign(passphrase, second_passphrase)
43+
)
44+
45+
assert builder.transaction.data['gasPrice'] == int(fixture['data']['gasPrice'])
46+
assert builder.transaction.data['gasLimit'] == int(fixture['data']['gasLimit'])
47+
assert builder.transaction.data['nonce'] == fixture['data']['nonce']
48+
assert builder.transaction.data['to'] == fixture['data']['to']
49+
assert builder.transaction.data['value'] == int(fixture['data']['value'])
50+
assert builder.transaction.data['v'] == fixture['data']['v']
51+
assert builder.transaction.data['r'] == fixture['data']['r']
52+
assert builder.transaction.data['s'] == fixture['data']['s']
53+
assert builder.transaction.data['legacySecondSignature'] == fixture['data']['legacySecondSignature']
54+
55+
assert builder.transaction.serialize().hex() == fixture['serialized']
56+
assert builder.transaction.data['hash'] == fixture['data']['hash']
57+
assert builder.verify()
58+
3159
def test_it_should_handle_unit_converter(passphrase, address):
3260
builder = (
3361
TransferBuilder

0 commit comments

Comments
 (0)