diff --git a/crypto/exceptions.py b/crypto/exceptions.py index 8c424c1..a3a85d3 100644 --- a/crypto/exceptions.py +++ b/crypto/exceptions.py @@ -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""" diff --git a/crypto/identity/proof_of_possession.py b/crypto/identity/proof_of_possession.py index 5df7443..b7ad052 100644 --- a/crypto/identity/proof_of_possession.py +++ b/crypto/identity/proof_of_possession.py @@ -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 @@ -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 + ) diff --git a/crypto/transactions/builder/validator_registration_builder.py b/crypto/transactions/builder/validator_registration_builder.py index a187251..56a6217 100644 --- a/crypto/transactions/builder/validator_registration_builder.py +++ b/crypto/transactions/builder/validator_registration_builder.py @@ -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() diff --git a/crypto/transactions/builder/validator_update_builder.py b/crypto/transactions/builder/validator_update_builder.py index 4adb898..c8a2d35 100644 --- a/crypto/transactions/builder/validator_update_builder.py +++ b/crypto/transactions/builder/validator_update_builder.py @@ -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() diff --git a/crypto/utils/transaction_data_encoder.py b/crypto/utils/transaction_data_encoder.py index 87433de..00481fa 100644 --- a/crypto/utils/transaction_data_encoder.py +++ b/crypto/utils/transaction_data_encoder.py @@ -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']] ) @@ -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']] ) diff --git a/setup.py b/setup.py index d74cbd5..9569f28 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ 'binary-helpers', 'coincurve', 'pycryptodomex', - 'btclib', + 'btclib==2023.7.12', 'cryptography', ] diff --git a/tests/fixtures/transactions/transaction-data-encoder.json b/tests/fixtures/transactions/transaction-data-encoder.json index 3e73e1f..4f0e2d0 100644 --- a/tests/fixtures/transactions/transaction-data-encoder.json +++ b/tests/fixtures/transactions/transaction-data-encoder.json @@ -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" diff --git a/tests/fixtures/transactions/validator-registration.json b/tests/fixtures/transactions/validator-registration.json index f47b3f2..f41dff3 100644 --- a/tests/fixtures/transactions/validator-registration.json +++ b/tests/fixtures/transactions/validator-registration.json @@ -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" } diff --git a/tests/fixtures/transactions/validator-update.json b/tests/fixtures/transactions/validator-update.json index 26db201..d2d5a48 100644 --- a/tests/fixtures/transactions/validator-update.json +++ b/tests/fixtures/transactions/validator-update.json @@ -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" } diff --git a/tests/identity/test_proof_of_possession.py b/tests/identity/test_proof_of_possession.py new file mode 100644 index 0000000..37764a9 --- /dev/null +++ b/tests/identity/test_proof_of_possession.py @@ -0,0 +1,95 @@ +import pytest + +from crypto.configuration.network import Network +from crypto.exceptions import InvalidProofOfPossessionException +from crypto.identity.proof_of_possession import POP_DST, ProofOfPossession +from crypto.networks.testnet import Testnet + +# Resolvable only once proof_of_possession has added the bundled BLS lib to sys.path +from ec import G1FromBytes, G2FromBytes # noqa: E402, I100 +from schemes import core_verify_mpl # noqa: E402, I100, I201 + +SECRET_KEY = '67d53f170b908cabb9eb326c3c337762d59289a8fec79f7bc9254b584b73265c' +REGISTRANT = '0x75545540230d5c3BEf023202d23CB74cFA723376' +OTHER_REGISTRANT = '0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd' + + +class ReferenceNetwork(Testnet): + def chain_id(self): + return 10_000 + + +@pytest.fixture +def reference_network(): + previous = Network.get_network() + Network.set_network(ReferenceNetwork()) + yield + Network.set_network(previous) + + +def verify(pop: dict, chain_id: int, registrant_address: str) -> bool: + public_key = bytes.fromhex(pop['pk']) + signature = G2FromBytes(bytes.fromhex(pop['pop'])) + message = ProofOfPossession.build_message(chain_id, registrant_address, public_key) + + return core_verify_mpl(G1FromBytes(public_key), message, signature, POP_DST) + + +def test_it_matches_the_reference_vector(reference_network): + pop = ProofOfPossession.build_proof_of_possession(bytes.fromhex(SECRET_KEY), REGISTRANT) + + assert pop['pk'] == ( + 'a7e75af9dd4d868a41ad2f5a5b021d653e31084261724fb4' + '0ae2f1b1c31c778d3b9464502d599cf6720723ec5c68b59d' + ) + assert pop['pop'] == ( + 'a892e94d8ed6d0fe8792dcb31b7c5116a7d138ad4bbbd044' + '780a7c314e86673e783850121dc34d0edfa2a2560c2f30a4' + '02f4fa5106ff71d5c69bc3027210ef90b3d3ae0a19ffc9f5' + '54b37aca72f3bb25788c3177514d94e041441ba9d029b3ba' + ) + + +def test_it_builds_a_100_byte_message(): + message = ProofOfPossession.build_message(10_000, REGISTRANT, bytes(48)) + + assert len(message) == 100 + assert message[:32] == (10_000).to_bytes(32, 'big') + assert message[32:52] == bytes.fromhex(REGISTRANT[2:]) + + +def test_it_binds_to_the_configured_chain_id_and_registrant(): + pop = ProofOfPossession.build_proof_of_possession(bytes.fromhex(SECRET_KEY), REGISTRANT) + chain_id = Network.get_network().chain_id() + + assert verify(pop, chain_id, REGISTRANT) is True + assert verify(pop, chain_id + 1, REGISTRANT) is False + assert verify(pop, chain_id, OTHER_REGISTRANT) is False + + +def test_it_accepts_a_lowercase_registrant(): + checksummed = ProofOfPossession.build_proof_of_possession(bytes.fromhex(SECRET_KEY), REGISTRANT) + lowercase = ProofOfPossession.build_proof_of_possession( + bytes.fromhex(SECRET_KEY), REGISTRANT.lower() + ) + + assert lowercase == checksummed + + +@pytest.mark.parametrize('registrant_address', [ + '', + None, + '75545540230d5c3BEf023202d23CB74cFA723376', + '0x75545540230d5c3BEf023202d23CB74cFA72337', + '0x75545540230d5c3BEf023202d23CB74cFA7233766', + '0x75545540230d5c3BEf023202d23CB74cFA72337g', + '0x75545540230d5c3bEf023202d23CB74cFA723376', +]) +def test_it_rejects_an_invalid_registrant(registrant_address): + with pytest.raises(InvalidProofOfPossessionException): + ProofOfPossession.build_proof_of_possession(bytes.fromhex(SECRET_KEY), registrant_address) + + +def test_it_rejects_an_invalid_registrant_from_passphrase(passphrase): + with pytest.raises(InvalidProofOfPossessionException): + ProofOfPossession.from_passphrase(passphrase, '0xinvalid') diff --git a/tests/transactions/builder/test_validator_registration_builder.py b/tests/transactions/builder/test_validator_registration_builder.py index 3ab8964..293e337 100644 --- a/tests/transactions/builder/test_validator_registration_builder.py +++ b/tests/transactions/builder/test_validator_registration_builder.py @@ -11,7 +11,7 @@ def test_validator_registration_transaction(passphrase, load_transaction_fixture .gas_limit(fixture['data']['gasLimit']) .nonce(fixture['data']['nonce']) .value(fixture['data']['value']) - .validator_passphrase(fixture['validatorPassphrase']) + .validator_proof(fixture['validatorPassphrase'], fixture['registrantAddress']) .to(fixture['data']['to']) .sign(passphrase) ) @@ -41,7 +41,7 @@ def test_validator_registration_transaction_with_default_to(passphrase, load_tra .gas_limit(fixture['data']['gasLimit']) .nonce(fixture['data']['nonce']) .value(fixture['data']['value']) - .validator_passphrase(fixture['validatorPassphrase']) + .validator_proof(fixture['validatorPassphrase'], fixture['registrantAddress']) .sign(passphrase) ) diff --git a/tests/transactions/builder/test_validator_update_builder.py b/tests/transactions/builder/test_validator_update_builder.py index f377f72..c538a68 100644 --- a/tests/transactions/builder/test_validator_update_builder.py +++ b/tests/transactions/builder/test_validator_update_builder.py @@ -10,7 +10,7 @@ def test_validator_update_transaction(passphrase, load_transaction_fixture): .gas_price(fixture['data']['gasPrice']) .gas_limit(fixture['data']['gasLimit']) .nonce(fixture['data']['nonce']) - .validator_passphrase(fixture['validatorPassphrase']) + .validator_proof(fixture['validatorPassphrase'], fixture['registrantAddress']) .to(fixture['data']['to']) .sign(passphrase) ) @@ -39,7 +39,7 @@ def test_validator_update_transaction_with_default_to(passphrase, load_transacti .gas_price(fixture['data']['gasPrice']) .gas_limit(fixture['data']['gasLimit']) .nonce(fixture['data']['nonce']) - .validator_passphrase(fixture['validatorPassphrase']) + .validator_proof(fixture['validatorPassphrase'], fixture['registrantAddress']) .sign(passphrase) ) diff --git a/tests/utils/test_transaction_data_encoder.py b/tests/utils/test_transaction_data_encoder.py index fd5f63a..56f57e5 100644 --- a/tests/utils/test_transaction_data_encoder.py +++ b/tests/utils/test_transaction_data_encoder.py @@ -24,7 +24,7 @@ def test_encode_username_resignation(load_transaction_fixture): def test_encode_validator_registration(load_transaction_fixture): fixture = load_transaction_fixture('transactions/transaction-data-encoder') encoded = TransactionDataEncoder.validator_registration( - fixture['ValidatorPassphrase'] + fixture['ValidatorPassphrase'], fixture['RegistrantAddress'] ) assert encoded == fixture['Encoded']['ValidatorRegistration'] @@ -32,7 +32,7 @@ def test_encode_validator_registration(load_transaction_fixture): def test_encode_update_validator(load_transaction_fixture): fixture = load_transaction_fixture('transactions/transaction-data-encoder') encoded = TransactionDataEncoder.update_validator( - fixture['ValidatorPassphrase'] + fixture['ValidatorPassphrase'], fixture['RegistrantAddress'] ) assert encoded == fixture['Encoded']['UpdateValidator']