Skip to content

Commit 12af477

Browse files
authored
feat: use passphrase for validator registration/update (#184)
1 parent 420e008 commit 12af477

16 files changed

Lines changed: 903 additions & 666 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import sys
2+
import os
3+
from os.path import dirname
4+
5+
sys.path.append(os.path.join(dirname(dirname(__file__)), 'thirdparty/bls-signatures/python-impl'))
6+
7+
from schemes import PopSchemeMPL, PrivateKey as BLSSchemePrivateKey
8+
9+
from crypto.identity.bls_private_key import BLSPrivateKey
10+
11+
12+
class ProofOfPossession:
13+
@staticmethod
14+
def derive_bls_private_key(passphrase: str) -> bytes:
15+
"""Derives a BLS private key (32 bytes) from a BIP-39 mnemonic."""
16+
return BLSPrivateKey.from_passphrase(passphrase).private_key
17+
18+
@classmethod
19+
def derive_bls_public_key(cls, passphrase: str) -> str:
20+
"""Derives the BLS12-381 G1 public key from a mnemonic. Returns hex (96 chars)."""
21+
sk = BLSSchemePrivateKey.from_bytes(cls.derive_bls_private_key(passphrase))
22+
return bytes(sk.get_g1()).hex()
23+
24+
@classmethod
25+
def build_proof_of_possession(cls, private_key_bytes: bytes) -> dict:
26+
"""Builds proof of possession for a given private key.
27+
28+
Args:
29+
private_key_bytes: 32-byte BLS private key
30+
31+
Returns:
32+
dict with 'pk' (hex, 48 bytes G1) and 'pop' (hex, 96 bytes G2)
33+
34+
Raises:
35+
ValueError: if the key is not exactly 32 bytes or is the zero scalar
36+
"""
37+
if int.from_bytes(private_key_bytes, 'big') == 0:
38+
raise ValueError('BLS secret key must not be zero')
39+
40+
sk = BLSSchemePrivateKey.from_bytes(private_key_bytes)
41+
pk = bytes(sk.get_g1()).hex()
42+
pop = bytes(PopSchemeMPL.pop_prove(sk)).hex()
43+
44+
return {'pk': pk, 'pop': pop}
45+
46+
@classmethod
47+
def from_passphrase(cls, passphrase: str) -> dict:
48+
"""Convenience: derives private key from mnemonic and builds PoP.
49+
50+
Returns:
51+
dict with 'pk' (hex, 48 bytes G1) and 'pop' (hex, 96 bytes G2)
52+
"""
53+
return cls.build_proof_of_possession(cls.derive_bls_private_key(passphrase))

crypto/transactions/builder/validator_registration_builder.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
from crypto.enums.contract_addresses import ContractAddresses
2+
from crypto.identity.proof_of_possession import ProofOfPossession
23
from crypto.transactions.builder.abstract_transaction_builder import AbstractTransactionBuilder
34
from crypto.transactions.types.validator_registration import ValidatorRegistration
45

6+
57
class ValidatorRegistrationBuilder(AbstractTransactionBuilder):
68
def __init__(self, data: dict):
79
super().__init__(data)
8-
910
self.to(ContractAddresses.CONSENSUS.value)
1011

11-
def validator_public_key(self, validator_public_key: str):
12-
self.transaction.data['validatorPublicKey'] = validator_public_key
12+
def validator_passphrase(self, passphrase: str):
13+
bls = ProofOfPossession.from_passphrase(passphrase)
14+
self.transaction.data['validatorPublicKey'] = '0x' + bls['pk']
15+
self.transaction.data['validatorProof'] = '0x' + bls['pop']
1316
self.transaction.refresh_payload_data()
1417
return self
1518

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from crypto.enums.contract_addresses import ContractAddresses
2+
from crypto.identity.proof_of_possession import ProofOfPossession
3+
from crypto.transactions.builder.abstract_transaction_builder import AbstractTransactionBuilder
4+
from crypto.transactions.types.validator_update import ValidatorUpdate
5+
6+
7+
class ValidatorUpdateBuilder(AbstractTransactionBuilder):
8+
def __init__(self, data: dict):
9+
super().__init__(data)
10+
self.to(ContractAddresses.CONSENSUS.value)
11+
12+
def validator_passphrase(self, passphrase: str):
13+
bls = ProofOfPossession.from_passphrase(passphrase)
14+
self.transaction.data['validatorPublicKey'] = '0x' + bls['pk']
15+
self.transaction.data['validatorProof'] = '0x' + bls['pop']
16+
self.transaction.refresh_payload_data()
17+
return self
18+
19+
def get_transaction_instance(self, data: dict):
20+
return ValidatorUpdate(data)

crypto/transactions/deserializer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from crypto.transactions.types.unvote import Unvote
1313
from crypto.transactions.types.validator_registration import ValidatorRegistration
1414
from crypto.transactions.types.validator_resignation import ValidatorResignation
15+
from crypto.transactions.types.validator_update import ValidatorUpdate
1516

1617
from crypto.enums.abi_function import AbiFunction
1718
from crypto.utils.abi_decoder import AbiDecoder
@@ -95,6 +96,9 @@ def __guess_transaction_from_data(self, data: dict) -> AbstractTransaction:
9596
if function_name == AbiFunction.VALIDATOR_RESIGNATION.value:
9697
return ValidatorResignation(data)
9798

99+
if function_name == AbiFunction.UPDATE_VALIDATOR.value:
100+
return ValidatorUpdate(data)
101+
98102
username_payload_data = self.decode_payload(data, ContractAbiType.USERNAMES)
99103
if username_payload_data is not None:
100104
function_name = username_payload_data.get('functionName')
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
from typing import Optional
21
from crypto.transactions.types.abstract_transaction import AbstractTransaction
32
from crypto.utils.abi_encoder import AbiEncoder
43
from crypto.enums.abi_function import AbiFunction
5-
from crypto.utils.transaction_utils import TransactionUtils
4+
65

76
class ValidatorRegistration(AbstractTransaction):
87
def __init__(self, data: dict):
98
payload = self._decode_payload(data)
109
if payload:
11-
data['validatorPublicKey'] = TransactionUtils.parse_hex_from_str(payload.get('args', [None])[0]) if payload.get('args') else None
10+
data['validatorPublicKey'], data['validatorProof'] = payload['args']
1211

1312
super().__init__(data)
1413

1514
def get_payload(self) -> str:
16-
if 'validatorPublicKey' not in self.data:
15+
if 'validatorPublicKey' not in self.data or 'validatorProof' not in self.data:
1716
return ''
1817
encoder = AbiEncoder()
19-
return encoder.encode_function_call(AbiFunction.VALIDATOR_REGISTRATION.value, ['0x' + self.data['validatorPublicKey']])
18+
return encoder.encode_function_call(
19+
AbiFunction.VALIDATOR_REGISTRATION.value,
20+
[self.data['validatorPublicKey'], self.data['validatorProof']],
21+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from crypto.transactions.types.abstract_transaction import AbstractTransaction
2+
from crypto.utils.abi_encoder import AbiEncoder
3+
from crypto.enums.abi_function import AbiFunction
4+
5+
6+
class ValidatorUpdate(AbstractTransaction):
7+
def __init__(self, data: dict):
8+
payload = self._decode_payload(data)
9+
if payload:
10+
data['validatorPublicKey'], data['validatorProof'] = payload['args']
11+
12+
super().__init__(data)
13+
14+
def get_payload(self) -> str:
15+
if 'validatorPublicKey' not in self.data or 'validatorProof' not in self.data:
16+
return ''
17+
encoder = AbiEncoder()
18+
return encoder.encode_function_call(
19+
AbiFunction.UPDATE_VALIDATOR.value,
20+
[self.data['validatorPublicKey'], self.data['validatorProof']],
21+
)

0 commit comments

Comments
 (0)