You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
9.8 KiB
304 lines
9.8 KiB
from typing import TYPE_CHECKING
|
|
|
|
from trezor.enums import (
|
|
CardanoCertificateType,
|
|
CardanoPoolRelayType,
|
|
CardanoTxSigningMode,
|
|
)
|
|
|
|
from apps.common import cbor
|
|
|
|
from .address import (
|
|
get_address_bytes_unsafe,
|
|
get_public_key_hash,
|
|
validate_reward_address,
|
|
)
|
|
from .helpers import ADDRESS_KEY_HASH_SIZE, INVALID_CERTIFICATE, LOVELACE_MAX_SUPPLY
|
|
from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT
|
|
from .helpers.utils import validate_stake_credential
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any
|
|
|
|
from trezor.messages import (
|
|
CardanoPoolMetadataType,
|
|
CardanoPoolOwner,
|
|
CardanoPoolParametersType,
|
|
CardanoPoolRelayParameters,
|
|
CardanoTxCertificate,
|
|
)
|
|
|
|
from apps.common.cbor import CborSequence
|
|
|
|
from . import seed
|
|
from .helpers.account_path_check import AccountPathChecker
|
|
|
|
POOL_HASH_SIZE = 28
|
|
VRF_KEY_HASH_SIZE = 32
|
|
POOL_METADATA_HASH_SIZE = 32
|
|
IPV4_ADDRESS_SIZE = 4
|
|
IPV6_ADDRESS_SIZE = 16
|
|
|
|
MAX_URL_LENGTH = 64
|
|
MAX_PORT_NUMBER = 65535
|
|
|
|
|
|
def validate_certificate(
|
|
certificate: CardanoTxCertificate,
|
|
signing_mode: CardanoTxSigningMode,
|
|
protocol_magic: int,
|
|
network_id: int,
|
|
account_path_checker: AccountPathChecker,
|
|
) -> None:
|
|
if (
|
|
signing_mode != CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER
|
|
and certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION
|
|
):
|
|
raise INVALID_CERTIFICATE
|
|
elif (
|
|
signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER
|
|
and certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION
|
|
):
|
|
raise INVALID_CERTIFICATE
|
|
|
|
_validate_certificate_structure(certificate)
|
|
|
|
if certificate.type in (
|
|
CardanoCertificateType.STAKE_DELEGATION,
|
|
CardanoCertificateType.STAKE_REGISTRATION,
|
|
CardanoCertificateType.STAKE_DEREGISTRATION,
|
|
):
|
|
validate_stake_credential(
|
|
certificate.path, certificate.script_hash, signing_mode, INVALID_CERTIFICATE
|
|
)
|
|
|
|
if certificate.type == CardanoCertificateType.STAKE_DELEGATION:
|
|
if not certificate.pool or len(certificate.pool) != POOL_HASH_SIZE:
|
|
raise INVALID_CERTIFICATE
|
|
|
|
if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION:
|
|
if certificate.pool_parameters is None:
|
|
raise INVALID_CERTIFICATE
|
|
_validate_pool_parameters(
|
|
certificate.pool_parameters, protocol_magic, network_id
|
|
)
|
|
|
|
account_path_checker.add_certificate(certificate)
|
|
|
|
|
|
def _validate_certificate_structure(certificate: CardanoTxCertificate) -> None:
|
|
path = certificate.path
|
|
script_hash = certificate.script_hash
|
|
pool = certificate.pool
|
|
pool_parameters = certificate.pool_parameters
|
|
|
|
fields_to_be_empty: dict[CardanoCertificateType, tuple[Any, ...]] = {
|
|
CardanoCertificateType.STAKE_REGISTRATION: (pool, pool_parameters),
|
|
CardanoCertificateType.STAKE_DELEGATION: (pool_parameters,),
|
|
CardanoCertificateType.STAKE_DEREGISTRATION: (pool, pool_parameters),
|
|
CardanoCertificateType.STAKE_POOL_REGISTRATION: (path, script_hash, pool),
|
|
}
|
|
|
|
if certificate.type not in fields_to_be_empty or any(
|
|
fields_to_be_empty[certificate.type]
|
|
):
|
|
raise INVALID_CERTIFICATE
|
|
|
|
|
|
def cborize_certificate(
|
|
keychain: seed.Keychain, certificate: CardanoTxCertificate
|
|
) -> CborSequence:
|
|
if certificate.type in (
|
|
CardanoCertificateType.STAKE_REGISTRATION,
|
|
CardanoCertificateType.STAKE_DEREGISTRATION,
|
|
):
|
|
return (
|
|
certificate.type,
|
|
cborize_certificate_stake_credential(
|
|
keychain, certificate.path, certificate.script_hash
|
|
),
|
|
)
|
|
elif certificate.type == CardanoCertificateType.STAKE_DELEGATION:
|
|
return (
|
|
certificate.type,
|
|
cborize_certificate_stake_credential(
|
|
keychain, certificate.path, certificate.script_hash
|
|
),
|
|
certificate.pool,
|
|
)
|
|
else:
|
|
raise INVALID_CERTIFICATE
|
|
|
|
|
|
def cborize_certificate_stake_credential(
|
|
keychain: seed.Keychain, path: list[int], script_hash: bytes | None
|
|
) -> tuple[int, bytes]:
|
|
if path:
|
|
return 0, get_public_key_hash(keychain, path)
|
|
|
|
if script_hash:
|
|
return 1, script_hash
|
|
|
|
# should be unreachable unless there's a bug in validation
|
|
raise INVALID_CERTIFICATE
|
|
|
|
|
|
def cborize_initial_pool_registration_certificate_fields(
|
|
certificate: CardanoTxCertificate,
|
|
) -> CborSequence:
|
|
assert certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION
|
|
|
|
pool_parameters = certificate.pool_parameters
|
|
assert pool_parameters is not None
|
|
|
|
return (
|
|
certificate.type,
|
|
pool_parameters.pool_id,
|
|
pool_parameters.vrf_key_hash,
|
|
pool_parameters.pledge,
|
|
pool_parameters.cost,
|
|
cbor.Tagged(
|
|
30,
|
|
(
|
|
pool_parameters.margin_numerator,
|
|
pool_parameters.margin_denominator,
|
|
),
|
|
),
|
|
# this relies on pool_parameters.reward_account being validated beforehand
|
|
# in _validate_pool_parameters
|
|
get_address_bytes_unsafe(pool_parameters.reward_account),
|
|
)
|
|
|
|
|
|
def assert_certificate_cond(condition: bool) -> None:
|
|
if not condition:
|
|
raise INVALID_CERTIFICATE
|
|
|
|
|
|
def _validate_pool_parameters(
|
|
pool_parameters: CardanoPoolParametersType, protocol_magic: int, network_id: int
|
|
) -> None:
|
|
assert_certificate_cond(len(pool_parameters.pool_id) == POOL_HASH_SIZE)
|
|
assert_certificate_cond(len(pool_parameters.vrf_key_hash) == VRF_KEY_HASH_SIZE)
|
|
assert_certificate_cond(0 <= pool_parameters.pledge <= LOVELACE_MAX_SUPPLY)
|
|
assert_certificate_cond(0 <= pool_parameters.cost <= LOVELACE_MAX_SUPPLY)
|
|
assert_certificate_cond(pool_parameters.margin_numerator >= 0)
|
|
assert_certificate_cond(pool_parameters.margin_denominator > 0)
|
|
assert_certificate_cond(
|
|
pool_parameters.margin_numerator <= pool_parameters.margin_denominator
|
|
)
|
|
assert_certificate_cond(pool_parameters.owners_count > 0)
|
|
|
|
validate_reward_address(pool_parameters.reward_account, protocol_magic, network_id)
|
|
|
|
if pool_parameters.metadata:
|
|
_validate_pool_metadata(pool_parameters.metadata)
|
|
|
|
|
|
def validate_pool_owner(
|
|
owner: CardanoPoolOwner, account_path_checker: AccountPathChecker
|
|
) -> None:
|
|
assert_certificate_cond(
|
|
owner.staking_key_hash is not None or owner.staking_key_path is not None
|
|
)
|
|
if owner.staking_key_hash is not None:
|
|
assert_certificate_cond(len(owner.staking_key_hash) == ADDRESS_KEY_HASH_SIZE)
|
|
if owner.staking_key_path:
|
|
assert_certificate_cond(
|
|
SCHEMA_STAKING_ANY_ACCOUNT.match(owner.staking_key_path)
|
|
)
|
|
|
|
account_path_checker.add_pool_owner(owner)
|
|
|
|
|
|
def validate_pool_relay(pool_relay: CardanoPoolRelayParameters) -> None:
|
|
if pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_IP:
|
|
assert_certificate_cond(
|
|
pool_relay.ipv4_address is not None or pool_relay.ipv6_address is not None
|
|
)
|
|
if pool_relay.ipv4_address is not None:
|
|
assert_certificate_cond(len(pool_relay.ipv4_address) == IPV4_ADDRESS_SIZE)
|
|
if pool_relay.ipv6_address is not None:
|
|
assert_certificate_cond(len(pool_relay.ipv6_address) == IPV6_ADDRESS_SIZE)
|
|
assert_certificate_cond(
|
|
pool_relay.port is not None and 0 <= pool_relay.port <= MAX_PORT_NUMBER
|
|
)
|
|
elif pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_NAME:
|
|
assert_certificate_cond(
|
|
pool_relay.host_name is not None
|
|
and len(pool_relay.host_name) <= MAX_URL_LENGTH
|
|
)
|
|
assert_certificate_cond(
|
|
pool_relay.port is not None and 0 <= pool_relay.port <= MAX_PORT_NUMBER
|
|
)
|
|
elif pool_relay.type == CardanoPoolRelayType.MULTIPLE_HOST_NAME:
|
|
assert_certificate_cond(
|
|
pool_relay.host_name is not None
|
|
and len(pool_relay.host_name) <= MAX_URL_LENGTH
|
|
)
|
|
else:
|
|
raise INVALID_CERTIFICATE
|
|
|
|
|
|
def _validate_pool_metadata(pool_metadata: CardanoPoolMetadataType) -> None:
|
|
assert_certificate_cond(len(pool_metadata.url) <= MAX_URL_LENGTH)
|
|
assert_certificate_cond(len(pool_metadata.hash) == POOL_METADATA_HASH_SIZE)
|
|
assert_certificate_cond(all((32 <= ord(c) < 127) for c in pool_metadata.url))
|
|
|
|
|
|
def cborize_pool_owner(keychain: seed.Keychain, pool_owner: CardanoPoolOwner) -> bytes:
|
|
if pool_owner.staking_key_path:
|
|
return get_public_key_hash(keychain, pool_owner.staking_key_path)
|
|
elif pool_owner.staking_key_hash:
|
|
return pool_owner.staking_key_hash
|
|
else:
|
|
raise ValueError
|
|
|
|
|
|
def _cborize_ipv6_address(ipv6_address: bytes | None) -> bytes | None:
|
|
if ipv6_address is None:
|
|
return None
|
|
|
|
# ipv6 addresses are serialized to CBOR as uint_32[4] little endian
|
|
assert len(ipv6_address) == IPV6_ADDRESS_SIZE
|
|
|
|
result = b""
|
|
for i in range(0, 4):
|
|
result += bytes(reversed(ipv6_address[i * 4 : i * 4 + 4]))
|
|
|
|
return result
|
|
|
|
|
|
def cborize_pool_relay(
|
|
pool_relay: CardanoPoolRelayParameters,
|
|
) -> CborSequence:
|
|
if pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_IP:
|
|
return (
|
|
pool_relay.type,
|
|
pool_relay.port,
|
|
pool_relay.ipv4_address,
|
|
_cborize_ipv6_address(pool_relay.ipv6_address),
|
|
)
|
|
elif pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_NAME:
|
|
return (
|
|
pool_relay.type,
|
|
pool_relay.port,
|
|
pool_relay.host_name,
|
|
)
|
|
elif pool_relay.type == CardanoPoolRelayType.MULTIPLE_HOST_NAME:
|
|
return (
|
|
pool_relay.type,
|
|
pool_relay.host_name,
|
|
)
|
|
else:
|
|
raise INVALID_CERTIFICATE
|
|
|
|
|
|
def cborize_pool_metadata(
|
|
pool_metadata: CardanoPoolMetadataType | None,
|
|
) -> CborSequence | None:
|
|
if not pool_metadata:
|
|
return None
|
|
|
|
return (pool_metadata.url, pool_metadata.hash)
|