1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-05-31 13:18:45 +00:00

feat(cardano): add support for CIP36 Catalyst registration format

This commit is contained in:
David Misiak 2022-09-16 15:58:45 +02:00 committed by matejcik
parent 02dad0225b
commit ab02f7a400
12 changed files with 550 additions and 60 deletions

View File

@ -69,6 +69,11 @@ enum CardanoTxAuxiliaryDataSupplementType {
CATALYST_REGISTRATION_SIGNATURE = 1; CATALYST_REGISTRATION_SIGNATURE = 1;
} }
enum CardanoCatalystRegistrationFormat {
CIP15 = 0;
CIP36 = 1;
}
enum CardanoTxSigningMode { enum CardanoTxSigningMode {
ORDINARY_TRANSACTION = 0; ORDINARY_TRANSACTION = 0;
POOL_REGISTRATION_AS_OWNER = 1; POOL_REGISTRATION_AS_OWNER = 1;
@ -355,11 +360,22 @@ message CardanoTxWithdrawal {
/** /**
* @embed * @embed
*/ */
message CardanoCatalystRegistrationParametersType { message CardanoCatalystRegistrationDelegation {
required bytes voting_public_key = 1; required bytes voting_public_key = 1;
required uint32 weight = 2;
}
/**
* @embed
*/
message CardanoCatalystRegistrationParametersType {
optional bytes voting_public_key = 1;
repeated uint32 staking_path = 2; repeated uint32 staking_path = 2;
required CardanoAddressParametersType reward_address_parameters = 3; required CardanoAddressParametersType reward_address_parameters = 3;
required uint64 nonce = 4; required uint64 nonce = 4;
optional CardanoCatalystRegistrationFormat format = 5 [default=CIP15];
repeated CardanoCatalystRegistrationDelegation delegations = 6; // mutually exclusive with voting_public_key; max 32 delegations
optional uint64 voting_purpose = 7;
} }
/** /**

View File

@ -1398,6 +1398,63 @@
"error_message": "Invalid auxiliary data" "error_message": "Invalid auxiliary data"
} }
}, },
{
"description": "transaction with both voting public key and delegations in catalyst registration",
"parameters": {
"protocol_magic": 764824073,
"network_id": 1,
"fee": 42,
"ttl": 10,
"validity_interval_start": null,
"certificates": [],
"withdrawals": [],
"auxiliary_data": {
"catalyst_registration_parameters": {
"voting_public_key": "38DA0B509D45BF6C87BD55594B92F97081D3923B8C1334B9B8D0BF13FC1C12D0",
"staking_path": "m/1852'/1815'/0'/2/0",
"reward_address_parameters": {
"addressType": 0,
"path": "m/1852'/1815'/0'/0/0",
"stakingPath": "m/1852'/1815'/0'/2/0"
},
"nonce": 140,
"format": 1,
"delegations": [
{
"voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 1
}
]
}
},
"inputs": [
{
"path": "m/1852'/1815'/0'/0/0",
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"prev_index": 0
}
],
"outputs": [
{
"address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r",
"amount": "1"
}
],
"mint": [],
"script_data_hash": null,
"collateral_inputs": [],
"required_signers": [],
"collateral_return": null,
"total_collateral": null,
"reference_inputs": [],
"signing_mode": "ORDINARY_TRANSACTION",
"additional_witness_requests": [],
"include_network_id": false
},
"result": {
"error_message": "Invalid auxiliary data"
}
},
{ {
"description": "Output datum hash has incorrect length", "description": "Output datum hash has incorrect length",
"parameters": { "parameters": {

View File

@ -1002,7 +1002,7 @@
} }
}, },
{ {
"description": "transaction with catalyst registration", "description": "transaction with CIP15 catalyst registration",
"parameters": { "parameters": {
"protocol_magic": 764824073, "protocol_magic": 764824073,
"network_id": 1, "network_id": 1,
@ -1064,6 +1064,149 @@
} }
} }
}, },
{
"description": "transaction with CIP36 catalyst registration and voting purpose not specified",
"parameters": {
"protocol_magic": 764824073,
"network_id": 1,
"fee": 42,
"ttl": 10,
"validity_interval_start": null,
"certificates": [],
"withdrawals": [],
"auxiliary_data": {
"catalyst_registration_parameters": {
"staking_path": "m/1852'/1815'/0'/2/0",
"reward_address_parameters": {
"addressType": 0,
"path": "m/1852'/1815'/0'/0/0",
"stakingPath": "m/1852'/1815'/0'/2/0"
},
"nonce": 22634813,
"format": 1,
"delegations": [
{
"voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 1
},
{
"voting_public_key": "2af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 2
}
]
}
},
"inputs": [
{
"path": "m/1852'/1815'/0'/0/0",
"prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7",
"prev_index": 0
}
],
"outputs": [
{
"address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r",
"amount": "1"
}
],
"mint": [],
"script_data_hash": null,
"collateral_inputs": [],
"required_signers": [],
"collateral_return": null,
"total_collateral": null,
"reference_inputs": [],
"signing_mode": "ORDINARY_TRANSACTION",
"additional_witness_requests": [],
"include_network_id": false
},
"result": {
"tx_hash": "15e4e382d913a743776b93d730fee3ca39bfa3ee203801205333bc9aad249612",
"witnesses": [
{
"type": 1,
"pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1",
"signature": "c984c65a5d6ee16c9cdd9fd332a5f64907f25438ef2d1e6d625bdd5c76d15acdf3e5700338b6b5c0ca30d25dd604e1b33ab5ee3459ff8ce3ca5a11e774a18605",
"chain_code": null
}
],
"auxiliary_data_supplement": {
"type": 1,
"auxiliary_data_hash": "9d4c00f5b5b67760931fd7ed9850ff8e14dcdf957685191ab4bc755c52f0ed56",
"catalyst_signature": "2671b8e668ffce235647ac89deda6cc222e7b31a3d44606c2723fcf711b29f9af1e30b0c6b4f87ba37ddf9f6adf0226c39c09e655255890644a3dc4e64c3a001"
}
}
},
{
"description": "transaction with CIP36 catalyst registration and OTHER voting purpose",
"parameters": {
"protocol_magic": 764824073,
"network_id": 1,
"fee": 42,
"ttl": 10,
"validity_interval_start": null,
"certificates": [],
"withdrawals": [],
"auxiliary_data": {
"catalyst_registration_parameters": {
"staking_path": "m/1852'/1815'/0'/2/0",
"reward_address_parameters": {
"addressType": 0,
"path": "m/1852'/1815'/0'/0/0",
"stakingPath": "m/1852'/1815'/0'/2/0"
},
"nonce": 22634813,
"format": 1,
"delegations": [
{
"voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 1
}
],
"voting_purpose": 1
}
},
"inputs": [
{
"path": "m/1852'/1815'/0'/0/0",
"prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7",
"prev_index": 0
}
],
"outputs": [
{
"address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r",
"amount": "1"
}
],
"mint": [],
"script_data_hash": null,
"collateral_inputs": [],
"required_signers": [],
"collateral_return": null,
"total_collateral": null,
"reference_inputs": [],
"signing_mode": "ORDINARY_TRANSACTION",
"additional_witness_requests": [],
"include_network_id": false
},
"result": {
"tx_hash": "98357cec961c4c2bfef747bb204a06945ab55077166ec4367b644882136b8b39",
"witnesses": [
{
"type": 1,
"pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1",
"signature": "9ac45a56c7002a8bca2121b9f0bae52a7201336b7528495c22d49b845b514d93a70ca1571e8a4dd418fbf4c260018c264843e54fbd2a8c6486e8f00f93cd5103",
"chain_code": null
}
],
"auxiliary_data_supplement": {
"type": 1,
"auxiliary_data_hash": "28b7ffa6800833bdfe5421739eaa21d4a49cde1d84e762b147001169f7c0a385",
"catalyst_signature": "ebc00c615f988c6fc2e132d4419a719f04bbec56fe2569a00746a9e9b0d6e5bdd0809515cb2522c773c991c5ae39834403654d36b37e70b14897c0e98c8c0a0c"
}
}
},
{ {
"description": "Testnet transaction", "description": "Testnet transaction",
"parameters": { "parameters": {
@ -1592,14 +1735,21 @@
], ],
"auxiliary_data": { "auxiliary_data": {
"catalyst_registration_parameters": { "catalyst_registration_parameters": {
"voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"staking_path": "m/1852'/1815'/0'/2/0", "staking_path": "m/1852'/1815'/0'/2/0",
"reward_address_parameters": { "reward_address_parameters": {
"addressType": 0, "addressType": 0,
"path": "m/1852'/1815'/0'/0/0", "path": "m/1852'/1815'/0'/0/0",
"stakingPath": "m/1852'/1815'/0'/2/0" "stakingPath": "m/1852'/1815'/0'/2/0"
}, },
"nonce": 22634813 "nonce": 22634813,
"format": 1,
"delegations": [
{
"voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 1
}
],
"voting_purpose": 0
} }
}, },
"mint": [], "mint": [],
@ -1614,37 +1764,37 @@
"include_network_id": false "include_network_id": false
}, },
"result": { "result": {
"tx_hash": "ee0dfef8b97857ebe7aa8935af50e9f8f608ff4054c0c034600750d722d90631", "tx_hash": "f98e1b5edfd376356eb211103bfae679380929bf7fbc40b3355a68e98111d091",
"witnesses": [ "witnesses": [
{ {
"type": 1, "type": 1,
"pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1",
"signature": "7d17407e4e8f8b89f8794c022408a84e6f7ef163957d9d7e8ebee4cf9b5c87750c7c559f3a2663441535eec88ebce8540e7d7ea30897de984b1053b818374007", "signature": "448d2e063f1dbc8662a9f6dea887549cbee7d8e4254124dd1aed08330f4ce165531a846b4ebc42e9944d85b99e878b4255860b960c5f4bd94d4feeb42295d402",
"chain_code": null "chain_code": null
}, },
{ {
"type": 1, "type": 1,
"pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16", "pub_key": "36a8ef21d5b98fdf23a27325cf643deaac35e912c835e35037f23d1061ae5b16",
"signature": "df62ec013a32d137c86931cec726d104cbc3193776026ec36d10450d9cbd289abc4c2d44311878b3aba035a8aec2c076522183027f9da046b586b5de5c460504", "signature": "5ba01fe1a043d3851236395a22982bfdf9d58d80ee963c042e2aa3bc0f8b35b99be18319710ade92edcf49b7185b5e8d91710f3acaa8d9e0f41bad1e3271a801",
"chain_code": null "chain_code": null
}, },
{ {
"type": 1, "type": 1,
"pub_key": "e90d7b0a6cf831b0042d37961dd528842860e77914e715bcece676c75353b812", "pub_key": "e90d7b0a6cf831b0042d37961dd528842860e77914e715bcece676c75353b812",
"signature": "e249396d227f1d0540e58b64610bdb990eb1f1db9b3bae4a3d4a8088679af4a3bab464a5c912f7041a5fabc37e3009b3e1f4d76e2406429a0ebed85b880ecd0c", "signature": "5595ab117629c0a3743e7081b315d937451d546525db43b7253a76662a24100d23baeaf232dc2cccfbdd624ec3439a20a3ca0914b71df0a766ba08f444d1a60d",
"chain_code": null "chain_code": null
}, },
{ {
"type": 1, "type": 1,
"pub_key": "bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e", "pub_key": "bc65be1b0b9d7531778a1317c2aa6de936963c3f9ac7d5ee9e9eda25e0c97c5e",
"signature": "0dfd139ce3e255664a77de7d199ce5e4f1a1238ec17a6acec4aaae79be2ccd9b1d21127164c059c8aea2c4b91292aaf352c824550db7594b59e4eca6455d3f03", "signature": "a130822ccf92dee7a9c357432c7e4b4c6f21fc6efac9c548d00162569bc748b19384ccdf6c132d68b04526658c3766e40cef7b45f73f5398b0db946469343005",
"chain_code": null "chain_code": null
} }
], ],
"auxiliary_data_supplement": { "auxiliary_data_supplement": {
"type": 1, "type": 1,
"auxiliary_data_hash": "a943e9166f1bb6d767b175384d3bd7d23645170df36fc1861fbf344135d8e120", "auxiliary_data_hash": "544c9ae849c82e31224865ff936decc6160047409eee4a6b4178b729fe3d286c",
"catalyst_signature": "74f27d877bbb4a5fc4f7c56869905c11f70bad0af3de24b23afaa1d024e750930f434ecc4b73e5d1723c2cb8548e8bf6098ac876487b3a6ed0891cb76994d409" "catalyst_signature": "3064949c9f186138f95e228075d0119dd5cb50e1b7e75d24d569fa547e018a597615da7c79a39ca8e394ee1ba8acb83e70be80f37e69aef3b86e7c4a6bd44903"
} }
} }
}, },

View File

@ -455,6 +455,79 @@
} }
] ]
} }
},
{
"description": "transaction with CIP36 catalyst registration and voting purpose not specified",
"parameters": {
"protocol_magic": 764824073,
"network_id": 1,
"fee": 42,
"ttl": 10,
"validity_interval_start": null,
"certificates": [],
"withdrawals": [],
"auxiliary_data": {
"catalyst_registration_parameters": {
"staking_path": "m/1852'/1815'/0'/2/0",
"reward_address_parameters": {
"addressType": 0,
"path": "m/1852'/1815'/0'/0/0",
"stakingPath": "m/1852'/1815'/0'/2/0"
},
"nonce": 22634813,
"format": 1,
"delegations": [
{
"voting_public_key": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 1
},
{
"voting_public_key": "2af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"weight": 2
}
]
}
},
"inputs": [
{
"path": "m/1852'/1815'/0'/0/0",
"prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7",
"prev_index": 0
}
],
"outputs": [
{
"address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r",
"amount": "1"
}
],
"mint": [],
"script_data_hash": null,
"collateral_inputs": [],
"required_signers": [],
"collateral_return": null,
"total_collateral": null,
"reference_inputs": [],
"signing_mode": "ORDINARY_TRANSACTION",
"additional_witness_requests": [],
"include_network_id": false
},
"result": {
"tx_hash": "15e4e382d913a743776b93d730fee3ca39bfa3ee203801205333bc9aad249612",
"witnesses": [
{
"type": 1,
"pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1",
"signature": "c984c65a5d6ee16c9cdd9fd332a5f64907f25438ef2d1e6d625bdd5c76d15acdf3e5700338b6b5c0ca30d25dd604e1b33ab5ee3459ff8ce3ca5a11e774a18605",
"chain_code": null
}
],
"auxiliary_data_supplement": {
"type": 1,
"auxiliary_data_hash": "9d4c00f5b5b67760931fd7ed9850ff8e14dcdf957685191ab4bc755c52f0ed56",
"catalyst_signature": "2671b8e668ffce235647ac89deda6cc222e7b31a3d44606c2723fcf711b29f9af1e30b0c6b4f87ba37ddf9f6adf0226c39c09e655255890644a3dc4e64c3a001"
}
}
} }
] ]
} }

View File

@ -419,6 +419,8 @@ if not utils.BITCOIN_ONLY:
import trezor.enums.BinanceTimeInForce import trezor.enums.BinanceTimeInForce
trezor.enums.CardanoAddressType trezor.enums.CardanoAddressType
import trezor.enums.CardanoAddressType import trezor.enums.CardanoAddressType
trezor.enums.CardanoCatalystRegistrationFormat
import trezor.enums.CardanoCatalystRegistrationFormat
trezor.enums.CardanoCertificateType trezor.enums.CardanoCertificateType
import trezor.enums.CardanoCertificateType import trezor.enums.CardanoCertificateType
trezor.enums.CardanoDerivationType trezor.enums.CardanoDerivationType

View File

@ -3,18 +3,22 @@ from typing import TYPE_CHECKING
from trezor import messages, wire from trezor import messages, wire
from trezor.crypto import hashlib from trezor.crypto import hashlib
from trezor.crypto.curve import ed25519 from trezor.crypto.curve import ed25519
from trezor.enums import CardanoAddressType, CardanoTxAuxiliaryDataSupplementType from trezor.enums import (
CardanoAddressType,
CardanoCatalystRegistrationFormat,
CardanoTxAuxiliaryDataSupplementType,
)
from apps.common import cbor from apps.common import cbor
from . import addresses from . import addresses, layout
from .helpers import bech32 from .helpers import bech32
from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT
from .helpers.utils import derive_public_key from .helpers.utils import derive_public_key
from .layout import confirm_catalyst_registration, show_auxiliary_data_hash
if TYPE_CHECKING: if TYPE_CHECKING:
CatalystRegistrationPayload = dict[int, bytes | int] Delegations = list[tuple[bytes, int]]
CatalystRegistrationPayload = dict[int, Delegations | bytes | int]
SignedCatalystRegistrationPayload = tuple[CatalystRegistrationPayload, bytes] SignedCatalystRegistrationPayload = tuple[CatalystRegistrationPayload, bytes]
CatalystRegistrationSignature = dict[int, bytes] CatalystRegistrationSignature = dict[int, bytes]
CatalystRegistration = dict[ CatalystRegistration = dict[
@ -30,6 +34,14 @@ CATALYST_REGISTRATION_HASH_SIZE = 32
METADATA_KEY_CATALYST_REGISTRATION = 61284 METADATA_KEY_CATALYST_REGISTRATION = 61284
METADATA_KEY_CATALYST_REGISTRATION_SIGNATURE = 61285 METADATA_KEY_CATALYST_REGISTRATION_SIGNATURE = 61285
MAX_DELEGATION_COUNT = 32
DEFAULT_VOTING_PURPOSE = 0
def assert_cond(condition: bool) -> None:
if not condition:
raise wire.ProcessError("Invalid auxiliary data")
def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None: def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None:
fields_provided = 0 fields_provided = 0
@ -41,31 +53,57 @@ def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None:
_validate_catalyst_registration_parameters( _validate_catalyst_registration_parameters(
auxiliary_data.catalyst_registration_parameters auxiliary_data.catalyst_registration_parameters
) )
assert_cond(fields_provided == 1)
if fields_provided != 1:
raise wire.ProcessError("Invalid auxiliary data")
def _validate_hash(auxiliary_data_hash: bytes) -> None: def _validate_hash(auxiliary_data_hash: bytes) -> None:
if len(auxiliary_data_hash) != AUXILIARY_DATA_HASH_SIZE: assert_cond(len(auxiliary_data_hash) == AUXILIARY_DATA_HASH_SIZE)
raise wire.ProcessError("Invalid auxiliary data")
def _validate_catalyst_registration_parameters( def _validate_catalyst_registration_parameters(
parameters: messages.CardanoCatalystRegistrationParametersType, parameters: messages.CardanoCatalystRegistrationParametersType,
) -> None: ) -> None:
if len(parameters.voting_public_key) != CATALYST_VOTING_PUBLIC_KEY_LENGTH: voting_key_fields_provided = 0
raise wire.ProcessError("Invalid auxiliary data") if parameters.voting_public_key is not None:
voting_key_fields_provided += 1
_validate_voting_public_key(parameters.voting_public_key)
if parameters.delegations:
voting_key_fields_provided += 1
assert_cond(parameters.format == CardanoCatalystRegistrationFormat.CIP36)
_validate_delegations(parameters.delegations)
assert_cond(voting_key_fields_provided == 1)
if not SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.staking_path): assert_cond(SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.staking_path))
raise wire.ProcessError("Invalid auxiliary data")
address_parameters = parameters.reward_address_parameters address_parameters = parameters.reward_address_parameters
if address_parameters.address_type == CardanoAddressType.BYRON: assert_cond(address_parameters.address_type != CardanoAddressType.BYRON)
raise wire.ProcessError("Invalid auxiliary data")
addresses.validate_address_parameters(address_parameters) addresses.validate_address_parameters(address_parameters)
if parameters.voting_purpose is not None:
assert_cond(parameters.format == CardanoCatalystRegistrationFormat.CIP36)
def _validate_voting_public_key(key: bytes) -> None:
assert_cond(len(key) == CATALYST_VOTING_PUBLIC_KEY_LENGTH)
def _validate_delegations(
delegations: list[messages.CardanoCatalystDelegation],
) -> None:
assert_cond(len(delegations) <= MAX_DELEGATION_COUNT)
for delegation in delegations:
_validate_voting_public_key(delegation.voting_public_key)
def _get_voting_purpose_to_serialize(
parameters: messages.CardanoCatalystRegistrationParametersType,
) -> int | None:
if parameters.format == CardanoCatalystRegistrationFormat.CIP15:
return None
if parameters.voting_purpose is None:
return DEFAULT_VOTING_PURPOSE
return parameters.voting_purpose
async def show( async def show(
ctx: wire.Context, ctx: wire.Context,
@ -83,10 +121,11 @@ async def show(
parameters, parameters,
protocol_magic, protocol_magic,
network_id, network_id,
should_show_details,
) )
if should_show_details: if should_show_details:
await show_auxiliary_data_hash(ctx, auxiliary_data_hash) await layout.show_auxiliary_data_hash(ctx, auxiliary_data_hash)
async def _show_catalyst_registration( async def _show_catalyst_registration(
@ -95,9 +134,22 @@ async def _show_catalyst_registration(
parameters: messages.CardanoCatalystRegistrationParametersType, parameters: messages.CardanoCatalystRegistrationParametersType,
protocol_magic: int, protocol_magic: int,
network_id: int, network_id: int,
should_show_details: bool,
) -> None: ) -> None:
public_key = parameters.voting_public_key for delegation in parameters.delegations:
encoded_public_key = bech32.encode(bech32.HRP_JORMUN_PUBLIC_KEY, public_key) encoded_public_key = bech32.encode(
bech32.HRP_JORMUN_PUBLIC_KEY, delegation.voting_public_key
)
await layout.confirm_catalyst_registration_delegation(
ctx, encoded_public_key, delegation.weight
)
encoded_public_key: str | None = None
if parameters.voting_public_key:
encoded_public_key = bech32.encode(
bech32.HRP_JORMUN_PUBLIC_KEY, parameters.voting_public_key
)
reward_address = addresses.derive_human_readable( reward_address = addresses.derive_human_readable(
keychain, keychain,
parameters.reward_address_parameters, parameters.reward_address_parameters,
@ -105,12 +157,17 @@ async def _show_catalyst_registration(
network_id, network_id,
) )
await confirm_catalyst_registration( voting_purpose: int | None = (
_get_voting_purpose_to_serialize(parameters) if should_show_details else None
)
await layout.confirm_catalyst_registration(
ctx, ctx,
encoded_public_key, encoded_public_key,
parameters.staking_path, parameters.staking_path,
reward_address, reward_address,
parameters.nonce, parameters.nonce,
voting_purpose,
) )
@ -172,19 +229,36 @@ def _get_signed_catalyst_registration_payload(
protocol_magic: int, protocol_magic: int,
network_id: int, network_id: int,
) -> SignedCatalystRegistrationPayload: ) -> SignedCatalystRegistrationPayload:
delegations_or_key: Delegations | bytes
if len(parameters.delegations) > 0:
delegations_or_key = [
(delegation.voting_public_key, delegation.weight)
for delegation in parameters.delegations
]
elif parameters.voting_public_key:
delegations_or_key = parameters.voting_public_key
else:
raise RuntimeError # should not be reached - _validate_governance_registration_parameters
staking_key = derive_public_key(keychain, parameters.staking_path) staking_key = derive_public_key(keychain, parameters.staking_path)
payload: CatalystRegistrationPayload = { reward_address = addresses.derive_bytes(
1: parameters.voting_public_key,
2: staking_key,
3: addresses.derive_bytes(
keychain, keychain,
parameters.reward_address_parameters, parameters.reward_address_parameters,
protocol_magic, protocol_magic,
network_id, network_id,
), )
voting_purpose = _get_voting_purpose_to_serialize(parameters)
payload: CatalystRegistrationPayload = {
1: delegations_or_key,
2: staking_key,
3: reward_address,
4: parameters.nonce, 4: parameters.nonce,
} }
if voting_purpose is not None:
payload[5] = voting_purpose
signature = _create_catalyst_registration_payload_signature( signature = _create_catalyst_registration_payload_signature(
keychain, keychain,

View File

@ -750,27 +750,61 @@ def _format_stake_credential(
raise ValueError raise ValueError
async def confirm_catalyst_registration( async def confirm_catalyst_registration_delegation(
ctx: wire.Context, ctx: wire.Context,
public_key: str, public_key: str,
weight: int,
) -> None:
props: list[PropertyType] = [
("Catalyst voting key registration", None),
("Delegating to:", public_key),
]
if weight is not None:
props.append(("Weight:", str(weight)))
await confirm_properties(
ctx,
"confirm_catalyst_registration_delegation",
title="Confirm transaction",
props=props,
br_code=ButtonRequestType.Other,
)
async def confirm_catalyst_registration(
ctx: wire.Context,
public_key: str | None,
staking_path: list[int], staking_path: list[int],
reward_address: str, reward_address: str,
nonce: int, nonce: int,
voting_purpose: int | None,
) -> None: ) -> None:
await confirm_properties( props: list[PropertyType] = [("Catalyst voting key registration", None)]
ctx, if public_key is not None:
"confirm_catalyst_registration", props.append(("Voting public key:", public_key))
title="Confirm transaction", props.extend(
props=[ [
("Catalyst voting key registration", None),
("Voting public key:", public_key),
( (
f"Staking key for account {format_account_number(staking_path)}:", f"Staking key for account {format_account_number(staking_path)}:",
address_n_to_str(staking_path), address_n_to_str(staking_path),
), ),
("Rewards go to:", reward_address), ("Rewards go to:", reward_address),
("Nonce:", str(nonce)), ("Nonce:", str(nonce)),
], ]
)
if voting_purpose is not None:
props.append(
(
"Voting purpose:",
"Catalyst" if voting_purpose == 0 else f"{voting_purpose} (other)",
)
)
await confirm_properties(
ctx,
"confirm_catalyst_registration",
title="Confirm transaction",
props=props,
br_code=ButtonRequestType.Other, br_code=ButtonRequestType.Other,
) )

View File

@ -0,0 +1,6 @@
# Automatically generated by pb2py
# fmt: off
# isort:skip_file
CIP15 = 0
CIP36 = 1

View File

@ -383,6 +383,10 @@ if TYPE_CHECKING:
NONE = 0 NONE = 0
CATALYST_REGISTRATION_SIGNATURE = 1 CATALYST_REGISTRATION_SIGNATURE = 1
class CardanoCatalystRegistrationFormat(IntEnum):
CIP15 = 0
CIP36 = 1
class CardanoTxSigningMode(IntEnum): class CardanoTxSigningMode(IntEnum):
ORDINARY_TRANSACTION = 0 ORDINARY_TRANSACTION = 0
POOL_REGISTRATION_AS_OWNER = 1 POOL_REGISTRATION_AS_OWNER = 1

View File

@ -24,6 +24,7 @@ if TYPE_CHECKING:
from trezor.enums import ButtonRequestType # noqa: F401 from trezor.enums import ButtonRequestType # noqa: F401
from trezor.enums import Capability # noqa: F401 from trezor.enums import Capability # noqa: F401
from trezor.enums import CardanoAddressType # noqa: F401 from trezor.enums import CardanoAddressType # noqa: F401
from trezor.enums import CardanoCatalystRegistrationFormat # noqa: F401
from trezor.enums import CardanoCertificateType # noqa: F401 from trezor.enums import CardanoCertificateType # noqa: F401
from trezor.enums import CardanoDerivationType # noqa: F401 from trezor.enums import CardanoDerivationType # noqa: F401
from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401 from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401
@ -1636,19 +1637,41 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxWithdrawal"]: def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxWithdrawal"]:
return isinstance(msg, cls) return isinstance(msg, cls)
class CardanoCatalystRegistrationParametersType(protobuf.MessageType): class CardanoCatalystRegistrationDelegation(protobuf.MessageType):
voting_public_key: "bytes" voting_public_key: "bytes"
staking_path: "list[int]" weight: "int"
reward_address_parameters: "CardanoAddressParametersType"
nonce: "int"
def __init__( def __init__(
self, self,
*, *,
voting_public_key: "bytes", voting_public_key: "bytes",
weight: "int",
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["CardanoCatalystRegistrationDelegation"]:
return isinstance(msg, cls)
class CardanoCatalystRegistrationParametersType(protobuf.MessageType):
voting_public_key: "bytes | None"
staking_path: "list[int]"
reward_address_parameters: "CardanoAddressParametersType"
nonce: "int"
format: "CardanoCatalystRegistrationFormat"
delegations: "list[CardanoCatalystRegistrationDelegation]"
voting_purpose: "int | None"
def __init__(
self,
*,
reward_address_parameters: "CardanoAddressParametersType", reward_address_parameters: "CardanoAddressParametersType",
nonce: "int", nonce: "int",
staking_path: "list[int] | None" = None, staking_path: "list[int] | None" = None,
delegations: "list[CardanoCatalystRegistrationDelegation] | None" = None,
voting_public_key: "bytes | None" = None,
format: "CardanoCatalystRegistrationFormat | None" = None,
voting_purpose: "int | None" = None,
) -> None: ) -> None:
pass pass

View File

@ -57,11 +57,11 @@ REQUIRED_FIELDS_POOL_PARAMETERS = (
) )
REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens") REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens")
REQUIRED_FIELDS_CATALYST_REGISTRATION = ( REQUIRED_FIELDS_CATALYST_REGISTRATION = (
"voting_public_key",
"staking_path", "staking_path",
"nonce", "nonce",
"reward_address_parameters", "reward_address_parameters",
) )
REQUIRED_FIELDS_CATALYST_DELEGATION = ("voting_public_key", "weight")
INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields"
@ -566,10 +566,27 @@ def parse_auxiliary_data(
): ):
raise AUXILIARY_DATA_MISSING_FIELDS_ERROR raise AUXILIARY_DATA_MISSING_FIELDS_ERROR
serialization_format = catalyst_registration.get("format")
delegations = []
for delegation in catalyst_registration.get("delegations", []):
if not all(k in delegation for k in REQUIRED_FIELDS_CATALYST_DELEGATION):
raise AUXILIARY_DATA_MISSING_FIELDS_ERROR
delegations.append(
messages.CardanoCatalystRegistrationDelegation(
voting_public_key=bytes.fromhex(delegation["voting_public_key"]),
weight=int(delegation["weight"]),
)
)
voting_purpose = None
if serialization_format == messages.CardanoCatalystRegistrationFormat.CIP36:
voting_purpose = catalyst_registration.get("voting_purpose")
catalyst_registration_parameters = ( catalyst_registration_parameters = (
messages.CardanoCatalystRegistrationParametersType( messages.CardanoCatalystRegistrationParametersType(
voting_public_key=bytes.fromhex( voting_public_key=parse_optional_bytes(
catalyst_registration["voting_public_key"] catalyst_registration.get("voting_public_key")
), ),
staking_path=tools.parse_path(catalyst_registration["staking_path"]), staking_path=tools.parse_path(catalyst_registration["staking_path"]),
nonce=catalyst_registration["nonce"], nonce=catalyst_registration["nonce"],
@ -577,6 +594,9 @@ def parse_auxiliary_data(
catalyst_registration["reward_address_parameters"], catalyst_registration["reward_address_parameters"],
str(AUXILIARY_DATA_MISSING_FIELDS_ERROR), str(AUXILIARY_DATA_MISSING_FIELDS_ERROR),
), ),
format=serialization_format,
delegations=delegations,
voting_purpose=voting_purpose,
) )
) )

View File

@ -408,6 +408,11 @@ class CardanoTxAuxiliaryDataSupplementType(IntEnum):
CATALYST_REGISTRATION_SIGNATURE = 1 CATALYST_REGISTRATION_SIGNATURE = 1
class CardanoCatalystRegistrationFormat(IntEnum):
CIP15 = 0
CIP36 = 1
class CardanoTxSigningMode(IntEnum): class CardanoTxSigningMode(IntEnum):
ORDINARY_TRANSACTION = 0 ORDINARY_TRANSACTION = 0
POOL_REGISTRATION_AS_OWNER = 1 POOL_REGISTRATION_AS_OWNER = 1
@ -2626,27 +2631,53 @@ class CardanoTxWithdrawal(protobuf.MessageType):
self.key_hash = key_hash self.key_hash = key_hash
class CardanoCatalystRegistrationParametersType(protobuf.MessageType): class CardanoCatalystRegistrationDelegation(protobuf.MessageType):
MESSAGE_WIRE_TYPE = None MESSAGE_WIRE_TYPE = None
FIELDS = { FIELDS = {
1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=True), 1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=True),
2: protobuf.Field("staking_path", "uint32", repeated=True, required=False), 2: protobuf.Field("weight", "uint32", repeated=False, required=True),
3: protobuf.Field("reward_address_parameters", "CardanoAddressParametersType", repeated=False, required=True),
4: protobuf.Field("nonce", "uint64", repeated=False, required=True),
} }
def __init__( def __init__(
self, self,
*, *,
voting_public_key: "bytes", voting_public_key: "bytes",
weight: "int",
) -> None:
self.voting_public_key = voting_public_key
self.weight = weight
class CardanoCatalystRegistrationParametersType(protobuf.MessageType):
MESSAGE_WIRE_TYPE = None
FIELDS = {
1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=False),
2: protobuf.Field("staking_path", "uint32", repeated=True, required=False),
3: protobuf.Field("reward_address_parameters", "CardanoAddressParametersType", repeated=False, required=True),
4: protobuf.Field("nonce", "uint64", repeated=False, required=True),
5: protobuf.Field("format", "CardanoCatalystRegistrationFormat", repeated=False, required=False),
6: protobuf.Field("delegations", "CardanoCatalystRegistrationDelegation", repeated=True, required=False),
7: protobuf.Field("voting_purpose", "uint64", repeated=False, required=False),
}
def __init__(
self,
*,
reward_address_parameters: "CardanoAddressParametersType", reward_address_parameters: "CardanoAddressParametersType",
nonce: "int", nonce: "int",
staking_path: Optional[Sequence["int"]] = None, staking_path: Optional[Sequence["int"]] = None,
delegations: Optional[Sequence["CardanoCatalystRegistrationDelegation"]] = None,
voting_public_key: Optional["bytes"] = None,
format: Optional["CardanoCatalystRegistrationFormat"] = CardanoCatalystRegistrationFormat.CIP15,
voting_purpose: Optional["int"] = None,
) -> None: ) -> None:
self.staking_path: Sequence["int"] = staking_path if staking_path is not None else [] self.staking_path: Sequence["int"] = staking_path if staking_path is not None else []
self.voting_public_key = voting_public_key self.delegations: Sequence["CardanoCatalystRegistrationDelegation"] = delegations if delegations is not None else []
self.reward_address_parameters = reward_address_parameters self.reward_address_parameters = reward_address_parameters
self.nonce = nonce self.nonce = nonce
self.voting_public_key = voting_public_key
self.format = format
self.voting_purpose = voting_purpose
class CardanoTxAuxiliaryData(protobuf.MessageType): class CardanoTxAuxiliaryData(protobuf.MessageType):