mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-16 01:22:02 +00:00
feat(core/cardano): chunked serialization of signed transaction
This commit is contained in:
parent
3cb686d452
commit
a9b8b0e119
@ -220,11 +220,27 @@ message CardanoSignTx {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response: Serialised signed cardano transaction chunk
|
||||
* @next CardanoSignedTxChunkAck
|
||||
*/
|
||||
message CardanoSignedTxChunk {
|
||||
required bytes signed_tx_chunk = 1; // serialised, signed transaction chunk
|
||||
}
|
||||
|
||||
/**
|
||||
* Request: Serialised signed cardano transaction chunk acknowledgement
|
||||
* @next CardanoSignedTxChunk
|
||||
* @next CardanoSignedTx
|
||||
*/
|
||||
message CardanoSignedTxChunkAck {
|
||||
}
|
||||
|
||||
/**
|
||||
* Response: Serialised signed cardano transaction
|
||||
* @end
|
||||
*/
|
||||
message CardanoSignedTx {
|
||||
required bytes tx_hash = 1; // hash of the transaction body
|
||||
required bytes serialized_tx = 2; // serialized, signed transaction
|
||||
required bytes tx_hash = 1; // hash of the transaction body
|
||||
optional bytes serialized_tx = 2; // deprecated since transaction is sent in chunks now - kept for backwards compatibility
|
||||
}
|
||||
|
@ -206,6 +206,8 @@ enum MessageType {
|
||||
MessageType_CardanoGetAddress = 307 [(wire_in) = true];
|
||||
MessageType_CardanoAddress = 308 [(wire_out) = true];
|
||||
MessageType_CardanoSignedTx = 310 [(wire_out) = true];
|
||||
MessageType_CardanoSignedTxChunk = 311 [(wire_out) = true];
|
||||
MessageType_CardanoSignedTxChunkAck = 312 [(wire_in) = true];
|
||||
|
||||
// Ripple
|
||||
MessageType_RippleGetAddress = 400 [(wire_in) = true];
|
||||
|
152
common/tests/fixtures/cardano/sign_tx.chunked.json
vendored
Normal file
152
common/tests/fixtures/cardano/sign_tx.chunked.json
vendored
Normal file
File diff suppressed because one or more lines are too long
108
common/tests/fixtures/cardano/sign_tx.failed.json
vendored
108
common/tests/fixtures/cardano/sign_tx.failed.json
vendored
@ -740,114 +740,6 @@
|
||||
"error_message": "Invalid certificate path"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Too many tokens in output",
|
||||
"parameters": {
|
||||
"protocol_magic": 764824073,
|
||||
"network_id": 1,
|
||||
"fee": 42,
|
||||
"ttl": 10,
|
||||
"certificates": [],
|
||||
"withdrawals": [],
|
||||
"metadata": "",
|
||||
"inputs": [
|
||||
{
|
||||
"path": "m/1852'/1815'/0'/0/0",
|
||||
"prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7",
|
||||
"prev_index": 0
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
|
||||
"amount": "3003112",
|
||||
"token_bundle": [
|
||||
{
|
||||
"policy_id": "95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39",
|
||||
"tokens": [
|
||||
{
|
||||
"asset_name_bytes": "01aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "7878754"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "04aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "05aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "06aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "07aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "08aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "09aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "1234"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"policy_id": "75a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39",
|
||||
"tokens": [
|
||||
{
|
||||
"asset_name_bytes": "10aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "13aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "14aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "16aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
},
|
||||
{
|
||||
"asset_name_bytes": "17aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"amount": "47"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"error_message": "Maximum tx output size"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Repeated asset name in multiasset token group",
|
||||
"parameters": {
|
||||
|
@ -4,6 +4,8 @@ from trezor.crypto.curve import ed25519
|
||||
from trezor.messages import CardanoAddressType, CardanoCertificateType
|
||||
from trezor.messages.CardanoAddressParametersType import CardanoAddressParametersType
|
||||
from trezor.messages.CardanoSignedTx import CardanoSignedTx
|
||||
from trezor.messages.CardanoSignedTxChunk import CardanoSignedTxChunk
|
||||
from trezor.messages.CardanoSignedTxChunkAck import CardanoSignedTxChunkAck
|
||||
|
||||
from apps.common import cbor, safety_checks
|
||||
from apps.common.paths import validate_path
|
||||
@ -78,13 +80,14 @@ if False:
|
||||
|
||||
CborizedTokenBundle = Dict[bytes, Dict[bytes, int]]
|
||||
CborizedTxOutput = Tuple[bytes, Union[int, Tuple[int, CborizedTokenBundle]]]
|
||||
|
||||
CborizedSignedTx = Tuple[Dict, Dict, Optional[cbor.Raw]]
|
||||
TxHash = bytes
|
||||
|
||||
METADATA_HASH_SIZE = 32
|
||||
MINTING_POLICY_ID_LENGTH = 28
|
||||
MAX_METADATA_LENGTH = 500
|
||||
MAX_ASSET_NAME_LENGTH = 32
|
||||
MAX_TX_OUTPUT_SIZE = 512
|
||||
MAX_TX_CHUNK_SIZE = 256
|
||||
|
||||
|
||||
@seed.with_keychain
|
||||
@ -96,44 +99,50 @@ async def sign_tx(
|
||||
|
||||
validate_network_info(msg.network_id, msg.protocol_magic)
|
||||
|
||||
if _has_stake_pool_registration(msg):
|
||||
return await _sign_stake_pool_registration_tx(ctx, msg, keychain)
|
||||
else:
|
||||
return await _sign_standard_tx(ctx, msg, keychain)
|
||||
|
||||
|
||||
async def _sign_standard_tx(
|
||||
ctx: wire.Context, msg: CardanoSignTx, keychain: seed.Keychain
|
||||
) -> CardanoSignedTx:
|
||||
try:
|
||||
for i in msg.inputs:
|
||||
await validate_path(
|
||||
ctx, keychain, i.address_n, SCHEMA_ADDRESS.match(i.address_n)
|
||||
if _has_stake_pool_registration(msg):
|
||||
cborized_tx, tx_hash = await _sign_stake_pool_registration_tx(
|
||||
ctx, msg, keychain
|
||||
)
|
||||
else:
|
||||
cborized_tx, tx_hash = await _sign_ordinary_tx(ctx, msg, keychain)
|
||||
|
||||
_validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id)
|
||||
_validate_certificates(msg.certificates, msg.protocol_magic, msg.network_id)
|
||||
_validate_withdrawals(msg.withdrawals)
|
||||
_validate_metadata(msg.metadata)
|
||||
signed_tx_chunks = cbor.encode_chunked(cborized_tx, MAX_TX_CHUNK_SIZE)
|
||||
|
||||
# display the transaction in UI
|
||||
await _show_standard_tx(ctx, keychain, msg)
|
||||
for signed_tx_chunk in signed_tx_chunks:
|
||||
response = CardanoSignedTxChunk(signed_tx_chunk=signed_tx_chunk)
|
||||
await ctx.call(response, CardanoSignedTxChunkAck)
|
||||
|
||||
# sign the transaction bundle and prepare the result
|
||||
serialized_tx, tx_hash = _serialize_tx(keychain, msg)
|
||||
tx = CardanoSignedTx(serialized_tx=serialized_tx, tx_hash=tx_hash)
|
||||
return CardanoSignedTx(tx_hash=tx_hash, serialized_tx=None)
|
||||
|
||||
except ValueError as e:
|
||||
if __debug__:
|
||||
log.exception(__name__, e)
|
||||
raise wire.ProcessError("Signing failed")
|
||||
|
||||
return tx
|
||||
|
||||
async def _sign_ordinary_tx(
|
||||
ctx: wire.Context, msg: CardanoSignTx, keychain: seed.Keychain
|
||||
) -> Tuple[CborizedSignedTx, TxHash]:
|
||||
for i in msg.inputs:
|
||||
await validate_path(
|
||||
ctx, keychain, i.address_n, SCHEMA_ADDRESS.match(i.address_n)
|
||||
)
|
||||
|
||||
_validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id)
|
||||
_validate_certificates(msg.certificates, msg.protocol_magic, msg.network_id)
|
||||
_validate_withdrawals(msg.withdrawals)
|
||||
_validate_metadata(msg.metadata)
|
||||
|
||||
# display the transaction in UI
|
||||
await _show_standard_tx(ctx, keychain, msg)
|
||||
|
||||
return _cborize_signed_tx(keychain, msg)
|
||||
|
||||
|
||||
async def _sign_stake_pool_registration_tx(
|
||||
ctx: wire.Context, msg: CardanoSignTx, keychain: seed.Keychain
|
||||
) -> CardanoSignedTx:
|
||||
) -> Tuple[CborizedSignedTx, TxHash]:
|
||||
"""
|
||||
We have a separate tx signing flow for stake pool registration because it's a
|
||||
transaction where the witnessable entries (i.e. inputs, withdrawals, etc.)
|
||||
@ -146,26 +155,16 @@ async def _sign_stake_pool_registration_tx(
|
||||
except the stake pool certificate itself and we provide a witness only to the
|
||||
user's staking key in the list of pool owners.
|
||||
"""
|
||||
try:
|
||||
_validate_stake_pool_registration_tx_structure(msg)
|
||||
_validate_stake_pool_registration_tx_structure(msg)
|
||||
|
||||
_ensure_no_signing_inputs(msg.inputs)
|
||||
_validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id)
|
||||
_validate_certificates(msg.certificates, msg.protocol_magic, msg.network_id)
|
||||
_validate_metadata(msg.metadata)
|
||||
_ensure_no_signing_inputs(msg.inputs)
|
||||
_validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id)
|
||||
_validate_certificates(msg.certificates, msg.protocol_magic, msg.network_id)
|
||||
_validate_metadata(msg.metadata)
|
||||
|
||||
await _show_stake_pool_registration_tx(ctx, keychain, msg)
|
||||
await _show_stake_pool_registration_tx(ctx, keychain, msg)
|
||||
|
||||
# sign the transaction bundle and prepare the result
|
||||
serialized_tx, tx_hash = _serialize_tx(keychain, msg)
|
||||
tx = CardanoSignedTx(serialized_tx=serialized_tx, tx_hash=tx_hash)
|
||||
|
||||
except ValueError as e:
|
||||
if __debug__:
|
||||
log.exception(__name__, e)
|
||||
raise wire.ProcessError("Signing failed")
|
||||
|
||||
return tx
|
||||
return _cborize_signed_tx(keychain, msg)
|
||||
|
||||
|
||||
def _has_stake_pool_registration(msg: CardanoSignTx) -> bool:
|
||||
@ -221,7 +220,6 @@ def _validate_outputs(
|
||||
)
|
||||
|
||||
_validate_token_bundle(output.token_bundle)
|
||||
_validate_max_tx_output_size(keychain, output, protocol_magic, network_id)
|
||||
|
||||
if total_amount > LOVELACE_MAX_SUPPLY:
|
||||
raise wire.ProcessError("Total transaction amount is out of range!")
|
||||
@ -255,29 +253,6 @@ def _validate_token_bundle(token_bundle: List[CardanoAssetGroupType]) -> None:
|
||||
seen_asset_name_bytes.add(asset_name_bytes)
|
||||
|
||||
|
||||
def _validate_max_tx_output_size(
|
||||
keychain: seed.Keychain,
|
||||
output: CardanoTxOutputType,
|
||||
protocol_magic: int,
|
||||
network_id: int,
|
||||
) -> None:
|
||||
"""
|
||||
This limitation is a mitigation measure to prevent sending
|
||||
large (especially change) outputs containing many tokens that Trezor
|
||||
would not be able to spend reliably given that
|
||||
currently the full Cardano transaction is held in-memory.
|
||||
Once Cardano-transaction signing is refactored to be streamed, this
|
||||
limit can be lifted
|
||||
"""
|
||||
cborized_output = _cborize_output(keychain, output, protocol_magic, network_id)
|
||||
serialized_output = cbor.encode(cborized_output)
|
||||
|
||||
if len(serialized_output) > MAX_TX_OUTPUT_SIZE:
|
||||
raise wire.ProcessError(
|
||||
"Maximum tx output size (%s bytes) exceeded!" % MAX_TX_OUTPUT_SIZE
|
||||
)
|
||||
|
||||
|
||||
def _ensure_no_signing_inputs(inputs: List[CardanoTxInputType]) -> None:
|
||||
if any(i.address_n for i in inputs):
|
||||
raise INVALID_STAKEPOOL_REGISTRATION_TX_INPUTS
|
||||
@ -316,7 +291,9 @@ def _validate_metadata(metadata: Optional[bytes]) -> None:
|
||||
raise INVALID_METADATA
|
||||
|
||||
|
||||
def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, bytes]:
|
||||
def _cborize_signed_tx(
|
||||
keychain: seed.Keychain, msg: CardanoSignTx
|
||||
) -> Tuple[CborizedSignedTx, TxHash]:
|
||||
tx_body = _cborize_tx_body(keychain, msg)
|
||||
tx_hash = _hash_tx_body(tx_body)
|
||||
|
||||
@ -333,9 +310,7 @@ def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, b
|
||||
if msg.metadata:
|
||||
metadata = cbor.Raw(bytes(msg.metadata))
|
||||
|
||||
serialized_tx = cbor.encode([tx_body, witnesses, metadata])
|
||||
|
||||
return serialized_tx, tx_hash
|
||||
return (tx_body, witnesses, metadata), tx_hash
|
||||
|
||||
|
||||
def _cborize_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict:
|
||||
@ -462,8 +437,13 @@ def _hash_metadata(metadata: bytes) -> bytes:
|
||||
|
||||
|
||||
def _hash_tx_body(tx_body: Dict) -> bytes:
|
||||
tx_body_cbor = cbor.encode(tx_body)
|
||||
return hashlib.blake2b(data=tx_body_cbor, outlen=32).digest()
|
||||
tx_body_cbor_chunks = cbor.encode_streamed(tx_body)
|
||||
|
||||
hashfn = hashlib.blake2b(outlen=32)
|
||||
for chunk in tx_body_cbor_chunks:
|
||||
hashfn.update(chunk)
|
||||
|
||||
return hashfn.digest()
|
||||
|
||||
|
||||
def _cborize_witnesses(
|
||||
|
@ -7,10 +7,10 @@ from micropython import const
|
||||
|
||||
from trezor import log, utils
|
||||
|
||||
from . import readers
|
||||
from . import readers, writers
|
||||
|
||||
if False:
|
||||
from typing import Any, Iterable, List, Tuple, Union
|
||||
from typing import Any, List, Tuple, Union, Iterator
|
||||
|
||||
Value = Any
|
||||
CborSequence = Union[List[Value], Tuple[Value, ...]]
|
||||
@ -55,7 +55,7 @@ def _header(typ: int, l: int) -> bytes:
|
||||
raise NotImplementedError("Length %d not suppported" % l)
|
||||
|
||||
|
||||
def _cbor_encode(value: Value) -> Iterable[bytes]:
|
||||
def _cbor_encode(value: Value) -> Iterator[bytes]:
|
||||
if isinstance(value, int):
|
||||
if value >= 0:
|
||||
yield _header(_CBOR_UNSIGNED_INT, value)
|
||||
@ -230,6 +230,47 @@ def encode(value: Value) -> bytes:
|
||||
return b"".join(_cbor_encode(value))
|
||||
|
||||
|
||||
def encode_streamed(value: Value) -> Iterator[bytes]:
|
||||
"""
|
||||
Returns the encoded value as an iterable of the individual
|
||||
CBOR "chunks", removing the need to reserve a continuous
|
||||
chunk of memory for the full serialized representation of the value
|
||||
"""
|
||||
return _cbor_encode(value)
|
||||
|
||||
|
||||
def encode_chunked(value: Value, max_chunk_size: int) -> Iterator[bytes]:
|
||||
"""
|
||||
Returns the encoded value as an iterable of chunks of a given size,
|
||||
removing the need to reserve a continuous chunk of memory for the
|
||||
full serialized representation of the value.
|
||||
"""
|
||||
if max_chunk_size <= 0:
|
||||
raise ValueError
|
||||
|
||||
chunks = encode_streamed(value)
|
||||
|
||||
chunk_buffer = writers.empty_bytearray(max_chunk_size)
|
||||
try:
|
||||
current_chunk_view = utils.BufferReader(next(chunks))
|
||||
while True:
|
||||
num_bytes_to_write = min(
|
||||
current_chunk_view.remaining_count(),
|
||||
max_chunk_size - len(chunk_buffer),
|
||||
)
|
||||
chunk_buffer.extend(current_chunk_view.read(num_bytes_to_write))
|
||||
|
||||
if len(chunk_buffer) >= max_chunk_size:
|
||||
yield chunk_buffer
|
||||
chunk_buffer[:] = bytes()
|
||||
|
||||
if current_chunk_view.remaining_count() == 0:
|
||||
current_chunk_view = utils.BufferReader(next(chunks))
|
||||
except StopIteration:
|
||||
if len(chunk_buffer) > 0:
|
||||
yield chunk_buffer
|
||||
|
||||
|
||||
def decode(cbor: bytes) -> Value:
|
||||
r = utils.BufferReader(cbor)
|
||||
res = _cbor_decode(r)
|
||||
|
@ -17,7 +17,7 @@ class CardanoSignedTx(p.MessageType):
|
||||
self,
|
||||
*,
|
||||
tx_hash: bytes,
|
||||
serialized_tx: bytes,
|
||||
serialized_tx: bytes = None,
|
||||
) -> None:
|
||||
self.tx_hash = tx_hash
|
||||
self.serialized_tx = serialized_tx
|
||||
@ -26,5 +26,5 @@ class CardanoSignedTx(p.MessageType):
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('tx_hash', p.BytesType, p.FLAG_REQUIRED),
|
||||
2: ('serialized_tx', p.BytesType, p.FLAG_REQUIRED),
|
||||
2: ('serialized_tx', p.BytesType, None),
|
||||
}
|
||||
|
27
core/src/trezor/messages/CardanoSignedTxChunk.py
Normal file
27
core/src/trezor/messages/CardanoSignedTxChunk.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
import protobuf as p
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoSignedTxChunk(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 311
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
signed_tx_chunk: bytes,
|
||||
) -> None:
|
||||
self.signed_tx_chunk = signed_tx_chunk
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('signed_tx_chunk', p.BytesType, p.FLAG_REQUIRED),
|
||||
}
|
14
core/src/trezor/messages/CardanoSignedTxChunkAck.py
Normal file
14
core/src/trezor/messages/CardanoSignedTxChunkAck.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
import protobuf as p
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoSignedTxChunkAck(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 312
|
@ -145,6 +145,8 @@ if not utils.BITCOIN_ONLY:
|
||||
CardanoGetAddress: Literal[307] = 307
|
||||
CardanoAddress: Literal[308] = 308
|
||||
CardanoSignedTx: Literal[310] = 310
|
||||
CardanoSignedTxChunk: Literal[311] = 311
|
||||
CardanoSignedTxChunkAck: Literal[312] = 312
|
||||
RippleGetAddress: Literal[400] = 400
|
||||
RippleAddress: Literal[401] = 401
|
||||
RippleSignTx: Literal[402] = 402
|
||||
|
@ -1,3 +1,5 @@
|
||||
import math
|
||||
|
||||
from common import *
|
||||
|
||||
from apps.common.cbor import (
|
||||
@ -5,6 +7,8 @@ from apps.common.cbor import (
|
||||
IndefiniteLengthArray,
|
||||
decode,
|
||||
encode,
|
||||
encode_chunked,
|
||||
encode_streamed,
|
||||
)
|
||||
|
||||
class TestCardanoCbor(unittest.TestCase):
|
||||
@ -90,5 +94,53 @@ class TestCardanoCbor(unittest.TestCase):
|
||||
self.assertEqual(encode(value_tuple), encoded)
|
||||
self.assertEqual(decode(encoded), val)
|
||||
|
||||
def test_encode_streamed(self):
|
||||
large_dict = {i: i for i in range(100)}
|
||||
encoded = encode(large_dict)
|
||||
|
||||
encoded_streamed = [
|
||||
bytes(item) for item in encode_streamed(large_dict)
|
||||
]
|
||||
|
||||
self.assertEqual(b''.join(encoded_streamed), encoded)
|
||||
|
||||
def test_encode_chunked(self):
|
||||
large_dict = {i: i for i in range(100)}
|
||||
encoded = encode(large_dict)
|
||||
|
||||
encoded_len = len(encoded)
|
||||
assert encoded_len == 354
|
||||
|
||||
arbitrary_encoded_len_factor = 59
|
||||
arbitrary_power_of_two = 64
|
||||
larger_than_encoded_len = encoded_len + 1
|
||||
|
||||
for max_chunk_size in [
|
||||
1,
|
||||
10,
|
||||
arbitrary_encoded_len_factor,
|
||||
arbitrary_power_of_two,
|
||||
encoded_len,
|
||||
larger_than_encoded_len
|
||||
]:
|
||||
encoded_chunks = [
|
||||
bytes(chunk) for chunk in encode_chunked(large_dict, max_chunk_size)
|
||||
]
|
||||
|
||||
expected_number_of_chunks = math.ceil(len(encoded) / max_chunk_size)
|
||||
self.assertEqual(len(encoded_chunks), expected_number_of_chunks)
|
||||
|
||||
# all chunks except the last should be of chunk_size
|
||||
for i in range(len(encoded_chunks) - 1):
|
||||
self.assertEqual(len(encoded_chunks[i]), max_chunk_size)
|
||||
|
||||
# last chunk should contain the remaining bytes or the whole chunk
|
||||
remaining_bytes = len(encoded) % max_chunk_size
|
||||
expected_last_chunk_size = remaining_bytes if remaining_bytes > 0 else max_chunk_size
|
||||
self.assertEqual(len(encoded_chunks[-1]), expected_last_chunk_size)
|
||||
|
||||
self.assertEqual(b''.join(encoded_chunks), encoded)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -17,7 +17,7 @@
|
||||
from ipaddress import ip_address
|
||||
from typing import List, Optional
|
||||
|
||||
from . import messages, tools
|
||||
from . import exceptions, messages, tools
|
||||
from .tools import expect
|
||||
|
||||
PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 42}
|
||||
@ -370,4 +370,15 @@ def sign_tx(
|
||||
)
|
||||
)
|
||||
|
||||
return response
|
||||
result = bytearray()
|
||||
while isinstance(response, messages.CardanoSignedTxChunk):
|
||||
result.extend(response.signed_tx_chunk)
|
||||
response = client.call(messages.CardanoSignedTxChunkAck())
|
||||
|
||||
if not isinstance(response, messages.CardanoSignedTx):
|
||||
raise exceptions.TrezorException("Unexpected response")
|
||||
|
||||
if response.serialized_tx is not None:
|
||||
result.extend(response.serialized_tx)
|
||||
|
||||
return messages.CardanoSignedTx(tx_hash=response.tx_hash, serialized_tx=result)
|
||||
|
@ -17,7 +17,7 @@ class CardanoSignedTx(p.MessageType):
|
||||
self,
|
||||
*,
|
||||
tx_hash: bytes,
|
||||
serialized_tx: bytes,
|
||||
serialized_tx: bytes = None,
|
||||
) -> None:
|
||||
self.tx_hash = tx_hash
|
||||
self.serialized_tx = serialized_tx
|
||||
@ -26,5 +26,5 @@ class CardanoSignedTx(p.MessageType):
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('tx_hash', p.BytesType, p.FLAG_REQUIRED),
|
||||
2: ('serialized_tx', p.BytesType, p.FLAG_REQUIRED),
|
||||
2: ('serialized_tx', p.BytesType, None),
|
||||
}
|
||||
|
27
python/src/trezorlib/messages/CardanoSignedTxChunk.py
Normal file
27
python/src/trezorlib/messages/CardanoSignedTxChunk.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
from .. import protobuf as p
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoSignedTxChunk(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 311
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
signed_tx_chunk: bytes,
|
||||
) -> None:
|
||||
self.signed_tx_chunk = signed_tx_chunk
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('signed_tx_chunk', p.BytesType, p.FLAG_REQUIRED),
|
||||
}
|
14
python/src/trezorlib/messages/CardanoSignedTxChunkAck.py
Normal file
14
python/src/trezorlib/messages/CardanoSignedTxChunkAck.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
from .. import protobuf as p
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoSignedTxChunkAck(p.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 312
|
@ -142,6 +142,8 @@ CardanoPublicKey: Literal[306] = 306
|
||||
CardanoGetAddress: Literal[307] = 307
|
||||
CardanoAddress: Literal[308] = 308
|
||||
CardanoSignedTx: Literal[310] = 310
|
||||
CardanoSignedTxChunk: Literal[311] = 311
|
||||
CardanoSignedTxChunkAck: Literal[312] = 312
|
||||
RippleGetAddress: Literal[400] = 400
|
||||
RippleAddress: Literal[401] = 401
|
||||
RippleSignTx: Literal[402] = 402
|
||||
|
@ -35,6 +35,8 @@ from .CardanoPoolRelayParametersType import CardanoPoolRelayParametersType
|
||||
from .CardanoPublicKey import CardanoPublicKey
|
||||
from .CardanoSignTx import CardanoSignTx
|
||||
from .CardanoSignedTx import CardanoSignedTx
|
||||
from .CardanoSignedTxChunk import CardanoSignedTxChunk
|
||||
from .CardanoSignedTxChunkAck import CardanoSignedTxChunkAck
|
||||
from .CardanoTokenType import CardanoTokenType
|
||||
from .CardanoTxCertificateType import CardanoTxCertificateType
|
||||
from .CardanoTxInputType import CardanoTxInputType
|
||||
|
@ -98,6 +98,49 @@ def test_cardano_sign_tx_failed(client, parameters, result):
|
||||
)
|
||||
|
||||
|
||||
@parametrize_using_common_fixtures("cardano/sign_tx.chunked.json")
|
||||
def test_cardano_sign_tx_with_multiple_chunks(client, parameters, result):
|
||||
inputs = [cardano.create_input(i) for i in parameters["inputs"]]
|
||||
outputs = [cardano.create_output(o) for o in parameters["outputs"]]
|
||||
certificates = [cardano.create_certificate(c) for c in parameters["certificates"]]
|
||||
withdrawals = [cardano.create_withdrawal(w) for w in parameters["withdrawals"]]
|
||||
|
||||
input_flow = parameters.get("input_flow", ())
|
||||
|
||||
expected_responses = [
|
||||
messages.PassphraseRequest(),
|
||||
messages.ButtonRequest(),
|
||||
messages.ButtonRequest(),
|
||||
]
|
||||
expected_responses += [
|
||||
messages.CardanoSignedTxChunk(signed_tx_chunk=bytes.fromhex(signed_tx_chunk))
|
||||
for signed_tx_chunk in result["signed_tx_chunks"]
|
||||
]
|
||||
expected_responses += [
|
||||
messages.CardanoSignedTx(tx_hash=bytes.fromhex(result["tx_hash"]))
|
||||
]
|
||||
|
||||
with client:
|
||||
client.set_input_flow(_to_device_actions(client, input_flow))
|
||||
client.set_expected_responses(expected_responses)
|
||||
|
||||
response = cardano.sign_tx(
|
||||
client=client,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
fee=parameters["fee"],
|
||||
ttl=parameters.get("ttl"),
|
||||
validity_interval_start=parameters.get("validity_interval_start"),
|
||||
certificates=certificates,
|
||||
withdrawals=withdrawals,
|
||||
metadata=bytes.fromhex(parameters["metadata"]),
|
||||
protocol_magic=parameters["protocol_magic"],
|
||||
network_id=parameters["network_id"],
|
||||
)
|
||||
assert response.tx_hash.hex() == result["tx_hash"]
|
||||
assert response.serialized_tx.hex() == result["serialized_tx"]
|
||||
|
||||
|
||||
def _to_device_actions(client, input_flow):
|
||||
if not input_flow:
|
||||
yield
|
||||
|
@ -62,11 +62,11 @@
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[stake_deregistration_account_larger_than_100]": "ffae5d78193ba3e989f025b67c544ce77ac4fde9d547e4588a9fe6f90354a4c2",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_protocol_magic_with_mainnet_network_id]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_transaction_with_mainnet_output]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[too_many_tokens_in_output]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[two_owners_with_path]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[unsupported_address_type]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_amount_is_too_large]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_has_non_staking_path]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0",
|
||||
"cardano-test_sign_tx.py::test_cardano_sign_tx_with_multiple_chunks[large_transaction_to_be_-377cb9ff": "4a46dd8ac42295d853646a55ca1b85023b2235af6155be663b1de10a6c98def2",
|
||||
"test_autolock.py::test_apply_auto_lock_delay": "38c720e0d29b7487060f2a0f8d256a5e5b4f735511e049c43f6ea62e560603ae",
|
||||
"test_autolock.py::test_apply_auto_lock_delay_out_of_range[0]": "6badfcdd682ecaf16311749ef7a6c07c6a4d0df402427c8dd5a48d476751ed77",
|
||||
"test_autolock.py::test_apply_auto_lock_delay_out_of_range[1]": "6badfcdd682ecaf16311749ef7a6c07c6a4d0df402427c8dd5a48d476751ed77",
|
||||
|
Loading…
Reference in New Issue
Block a user