Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crypto/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ class ArkInvalidTransaction(ArkCryptoException):

class InvalidUsernameException(Exception):
"""Raised when username is invalid"""


class InvalidProofOfPossessionException(ArkCryptoException):
"""Raised when a proof of possession can't be built from the given input"""
53 changes: 44 additions & 9 deletions crypto/identity/proof_of_possession.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

sys.path.append(os.path.join(dirname(dirname(__file__)), 'thirdparty/bls-signatures/python-impl'))

from schemes import PopSchemeMPL, PrivateKey as BLSSchemePrivateKey
from schemes import PrivateKey as BLSSchemePrivateKey, core_sign_mpl

from crypto.configuration.network import Network
from crypto.exceptions import InvalidProofOfPossessionException
from crypto.identity.address import Address
from crypto.identity.bls_private_key import BLSPrivateKey

POP_DST = b'MAINSAIL_BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_'


class ProofOfPossession:
@staticmethod
Expand All @@ -21,33 +26,63 @@ def derive_bls_public_key(cls, passphrase: str) -> str:
sk = BLSSchemePrivateKey.from_bytes(cls.derive_bls_private_key(passphrase))
return bytes(sk.get_g1()).hex()

@staticmethod
def build_message(chain_id: int, registrant_address: str, public_key: bytes) -> bytes:
"""abi.encodePacked(uint256 chainId, address registrantAddress, bytes blsPublicKey)"""
return chain_id.to_bytes(32, 'big') + bytes.fromhex(registrant_address[2:]) + public_key

@staticmethod
def validate_registrant_address(registrant_address: str) -> None:
if not isinstance(registrant_address, str) or not Address.validate(registrant_address):
raise InvalidProofOfPossessionException(
f'Invalid registrant address: {registrant_address}'
)

hex_part = registrant_address[2:]
is_single_case = hex_part == hex_part.lower() or hex_part == hex_part.upper()

checksum_address = Address.get_checksum_address(registrant_address)

if not is_single_case and checksum_address != registrant_address:
raise InvalidProofOfPossessionException(
f'Invalid registrant address checksum: {registrant_address}'
)

@classmethod
def build_proof_of_possession(cls, private_key_bytes: bytes) -> dict:
"""Builds proof of possession for a given private key.
def build_proof_of_possession(cls, private_key_bytes: bytes, registrant_address: str) -> dict:
"""Builds a proof of possession bound to the registrant address and the configured
network chain ID.

Args:
private_key_bytes: 32-byte BLS private key
registrant_address: address submitting the registration or update

Returns:
dict with 'pk' (hex, 48 bytes G1) and 'pop' (hex, 96 bytes G2)

Raises:
ValueError: if the key is not exactly 32 bytes or is the zero scalar
InvalidProofOfPossessionException: if the registrant address is invalid
"""
if int.from_bytes(private_key_bytes, 'big') == 0:
raise ValueError('BLS secret key must not be zero')

sk = BLSSchemePrivateKey.from_bytes(private_key_bytes)
pk = bytes(sk.get_g1()).hex()
pop = bytes(PopSchemeMPL.pop_prove(sk)).hex()
cls.validate_registrant_address(registrant_address)

sk = BLSSchemePrivateKey.from_bytes(private_key_bytes)
pk = bytes(sk.get_g1())
message = cls.build_message(Network.get_network().chain_id(), registrant_address, pk)
pop = bytes(core_sign_mpl(sk, message, POP_DST))

return {'pk': pk, 'pop': pop}
return {'pk': pk.hex(), 'pop': pop.hex()}

@classmethod
def from_passphrase(cls, passphrase: str) -> dict:
def from_passphrase(cls, passphrase: str, registrant_address: str) -> dict:
"""Convenience: derives private key from mnemonic and builds PoP.

Returns:
dict with 'pk' (hex, 48 bytes G1) and 'pop' (hex, 96 bytes G2)
"""
return cls.build_proof_of_possession(cls.derive_bls_private_key(passphrase))
return cls.build_proof_of_possession(
cls.derive_bls_private_key(passphrase), registrant_address
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def __init__(self, data: dict):
super().__init__(data)
self.to(ContractAddresses.CONSENSUS.value)

def validator_passphrase(self, passphrase: str):
bls = ProofOfPossession.from_passphrase(passphrase)
def validator_proof(self, passphrase: str, registrant_address: str):
bls = ProofOfPossession.from_passphrase(passphrase, registrant_address)
self.transaction.data['validatorPublicKey'] = '0x' + bls['pk']
self.transaction.data['validatorProof'] = '0x' + bls['pop']
self.transaction.refresh_payload_data()
Expand Down
4 changes: 2 additions & 2 deletions crypto/transactions/builder/validator_update_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def __init__(self, data: dict):
super().__init__(data)
self.to(ContractAddresses.CONSENSUS.value)

def validator_passphrase(self, passphrase: str):
bls = ProofOfPossession.from_passphrase(passphrase)
def validator_proof(self, passphrase: str, registrant_address: str):
bls = ProofOfPossession.from_passphrase(passphrase, registrant_address)
self.transaction.data['validatorPublicKey'] = '0x' + bls['pk']
self.transaction.data['validatorProof'] = '0x' + bls['pop']
self.transaction.refresh_payload_data()
Expand Down
8 changes: 4 additions & 4 deletions crypto/utils/transaction_data_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ def multi_payment(recipients, amounts):
)

@staticmethod
def update_validator(passphrase: str):
bls = ProofOfPossession.from_passphrase(passphrase)
def update_validator(passphrase: str, registrant_address: str):
bls = ProofOfPossession.from_passphrase(passphrase, registrant_address)
return AbiEncoder(ContractAbiType.CONSENSUS).encode_function_call_hex(
AbiFunction.UPDATE_VALIDATOR.value, ['0x' + bls['pk'], '0x' + bls['pop']]
)
Expand All @@ -31,8 +31,8 @@ def username_resignation():
)

@staticmethod
def validator_registration(passphrase: str):
bls = ProofOfPossession.from_passphrase(passphrase)
def validator_registration(passphrase: str, registrant_address: str):
bls = ProofOfPossession.from_passphrase(passphrase, registrant_address)
return AbiEncoder(ContractAbiType.CONSENSUS).encode_function_call_hex(
AbiFunction.VALIDATOR_REGISTRATION.value, ['0x' + bls['pk'], '0x' + bls['pop']]
)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'binary-helpers',
'coincurve',
'pycryptodomex',
'btclib',
'btclib==2023.7.12',

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btclib was unpinned, so CI installed whatever was newest. Releases from 2026.9.29 onward no longer ship btclib.mnemonic, which crypto/identity/bls_private_key.py imports (from btclib.mnemonic import bip39). As a result, every BLS-related test failed at import on fresh installs.

2023.7.12 is the newest release that still supports Python 3.9 (all 2026.x releases require ≥3.10). Our CI matrix covers 3.9–3.12, so this is the only version that installs on all of them. On 3.9, pip was already resolving to it anyway.

'cryptography',
]

Expand Down
5 changes: 3 additions & 2 deletions tests/fixtures/transactions/transaction-data-encoder.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
"Username": "test",
"Address": "0xA5cc0BfEB09742C5e4C610f2EBaaB82Eb142Ca10",
"ValidatorPassphrase": "bless organ december boring ill obvious unaware dinosaur broccoli build hamster rebuild skin airport stay entry denial agent october thought duck trouble decorate way",
"RegistrantAddress": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd",
"Amount": "1000000000000",
"Encoded": {
"UpdateValidator": "0x8f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3",
"UpdateValidator": "0x8f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb61100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006097ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c",
"UsernameRegistration": "0x36a94134000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000",
"UsernameResignation": "0xebed6dab",
"ValidatorRegistration": "0x226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3",
"ValidatorRegistration": "0x226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb61100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006097ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c",
"ValidatorResignation": "0xb85f5da2",
"TokenTransfer": "0xa9059cbb000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10000000000000000000000000000000000000000000000000000000e8d4a51000",
"MultiPayment": "0x084ce708000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc0bfeb09742c5e4c610f2ebaab82eb142ca10000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003e8"
Expand Down
15 changes: 8 additions & 7 deletions tests/fixtures/transactions/validator-registration.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
"gasPrice": 5000000000,
"gasLimit": 200000,
"nonce": "1",
"data": "226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3",
"data": "226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb61100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006097ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c",
"to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1",
"validatorPublicKey": "0xb0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611",
"validatorProof": "0xa8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3",
"validatorProof": "0x97ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c",
"v": 0,
"r": "d44a597b3b2b14f9770abcdabd73b3c12d0a5ad1a289ac71f8f7a54212cf035d",
"s": "7a111e447b0b28462b7973020f6b8fa9e3c0aac6f5dd71ec1e1d6eb342e153c0",
"hash": "879559d0f2deed36c3e03fc513ba7f0940f3e1370f3c4750453266efc9066941"
"r": "9859f5cf72a93cfc2893e00915e288d668f867dbd4b4352809f6f2b560330552",
"s": "34b1ed8fb81832dd7ccd1f19e2b5d8b46d878974fe4a0b193876f2db6711abca",
"hash": "0b2e7b0e36333975114b7e27262edd9078027f87cc857ca9a8605be26f007a5a"
},
"serialized": "f901960185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e1890d8d726b7177a80000b90124226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3825c6ba0d44a597b3b2b14f9770abcdabd73b3c12d0a5ad1a289ac71f8f7a54212cf035da07a111e447b0b28462b7973020f6b8fa9e3c0aac6f5dd71ec1e1d6eb342e153c0",
"validatorPassphrase": "bless organ december boring ill obvious unaware dinosaur broccoli build hamster rebuild skin airport stay entry denial agent october thought duck trouble decorate way"
"serialized": "f901960185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e1890d8d726b7177a80000b90124226f2645000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb61100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006097ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c825c6ba09859f5cf72a93cfc2893e00915e288d668f867dbd4b4352809f6f2b560330552a034b1ed8fb81832dd7ccd1f19e2b5d8b46d878974fe4a0b193876f2db6711abca",
"validatorPassphrase": "bless organ december boring ill obvious unaware dinosaur broccoli build hamster rebuild skin airport stay entry denial agent october thought duck trouble decorate way",
"registrantAddress": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd"
}
17 changes: 9 additions & 8 deletions tests/fixtures/transactions/validator-update.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
"gasPrice": 5000000000,
"gasLimit": 200000,
"nonce": "1",
"data": "8f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3",
"data": "8f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb61100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006097ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c",
"to": "0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1",
"validatorPublicKey": "0xb0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611",
"validatorProof": "0xa8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3",
"v": 0,
"r": "583edde415ec3f61643b5e2a18c34c4113eb67434adfc4d3633a1469a6fe3895",
"s": "165b3ec00a56227ab3931865a3a63e5e5dbb79e4e42985abe394f8eb74acbc58",
"hash": "917fb0ebbdfdeb2785db0e3d67bb68bf655a6381a529f22676a7a16e07278e1c"
"validatorProof": "0x97ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c",
"v": 1,
"r": "d25b02f2b2af965d1c21bc88a168b9194b7d52f8ed497fe8fccdaa445cac9c2b",
"s": "301c1977071c3f8169ddbf306219fcebdafe25fe5b392f7dca819bb8d80aee0b",
"hash": "1fe4693b1b89acdead9cc7e3fcf11bc4e5970736946d4916335d09f3b48248a0"
},
"serialized": "f9018d0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180b901248f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb611000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a8bb7c80d24ee79cf3f141eae774b7575466d68631834c29433036ac84eb4904b8f071167cccd5bcc9d11e7c187d6fc70b87ce6e046a750b07f6a8a5985e76bf005bb55b8c70c1cefaccd4c37c314ce69ddfd2eb4a21f8775e01da02e77590b3825c6ba0583edde415ec3f61643b5e2a18c34c4113eb67434adfc4d3633a1469a6fe3895a0165b3ec00a56227ab3931865a3a63e5e5dbb79e4e42985abe394f8eb74acbc58",
"validatorPassphrase": "bless organ december boring ill obvious unaware dinosaur broccoli build hamster rebuild skin airport stay entry denial agent october thought duck trouble decorate way"
"serialized": "f9018d0185012a05f20083030d4094535b3d7a252fa034ed71f0c53ec0c6f784cb64e180b901248f062626000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000030b0093ac8f37588e15df7cfb04d0722dc5486cec062233136d3b6a16d41946577a1f332c4c29c0601ccefac1905dbb61100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006097ec59b22ef8232c90bf875c21f8e4452fef8f99f49bc0fc3bbcd2647c64d57fdcade7949ef2c9f0e3fe74553aca02880a2940ff0203523d790fd858f5a70c72c16c5718f1ca92bba37c1c9efbce445d3e071eb1fc8df5a7b1a2d54055939d8c825c6ca0d25b02f2b2af965d1c21bc88a168b9194b7d52f8ed497fe8fccdaa445cac9c2ba0301c1977071c3f8169ddbf306219fcebdafe25fe5b392f7dca819bb8d80aee0b",
"validatorPassphrase": "bless organ december boring ill obvious unaware dinosaur broccoli build hamster rebuild skin airport stay entry denial agent october thought duck trouble decorate way",
"registrantAddress": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd"
}
Loading
Loading