1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-27 00:48:12 +00:00

feat(core,common): Eth definitions signed using Merkle tree

This commit is contained in:
Martin Novak 2022-09-09 09:41:19 +02:00
parent fbacd4cc07
commit 7bf1c85897
11 changed files with 489 additions and 215 deletions

View File

@ -52,7 +52,7 @@ message EthereumAddress {
/** /**
* Ethereum definitions type enum. * Ethereum definitions type enum.
* Used to check the encoded EthereumNetworkInfo and/or EthereumTokenInfo message. * Used to check the encoded EthereumNetworkInfo or EthereumTokenInfo message.
*/ */
enum EthereumDefinitionType { enum EthereumDefinitionType {
NETWORK = 0; NETWORK = 0;

View File

@ -21,16 +21,24 @@ Following packets has the following structure:
| 1 | 63 | uint8_t[63] | following bytes of message encoded in Protocol Buffers (padded with zeroes if shorter) | | 1 | 63 | uint8_t[63] | following bytes of message encoded in Protocol Buffers (padded with zeroes if shorter) |
## Ethereum network and token definitions ## Ethereum network and token definitions
Ethereum network and token definitions could be sent from host to device. Definitions are generated in our CI job and are provided Ethereum network and token definitions could be sent from host to device. External definitions are generated from time to time
on publicly accessible website - on `data.trezor.io`. To ensure, that the definitions send to device are genuine, every generated and provided on publicly accessible website - `data.trezor.io` # TODO: update url. For more info look
definition is signed and checked in FW. at the [definitions README.md](../defs/README.md#eth-and-erc20).
Every definition (network/token) is encoded and has special format: To ensure that the definitions send to device are genuine we use Merkle tree with a combination of signed Merkle tree root hash.
Every definition is then checked and verified at FW side on receiving.
Definitions (network/token) are binary encoded and have a special format:
1. prefix: 1. prefix:
1. part: format version of the definition (UTF-8 string `trzd` + version number, padded with zeroes if shorter, 8 bytes) 1. format version of the definition (UTF-8 string `trzd` + version number, padded with zeroes if shorter, 8 bytes)
2. part: type of data (unsigned integer, 1 byte) 2. type of data (unsigned integer, 1 byte)
3. part: data version of the definition (unsigned integer, 4 bytes) 3. data version of the definition (unsigned integer, 4 bytes)
2. payload: serialized form of protobuf message EthereumNetworkInfo and/or EthereumTokenInfo (N bytes) 4. length of the encoded protobuf message - payload length in bytes (unsigned integer, 2 bytes)
3. suffix: signature of prefix+payload (plain bits, 64 bytes) 2. payload: serialized form of protobuf message EthereumNetworkInfo or EthereumTokenInfo (N bytes)
3. suffix:
1. length of the Merkle tree proof - number of hashes in the proof (unsigned integer, 1 byte)
2. proof - individual hashes used to compute Merkle tree root hash (plain bits, N*32 bytes)
3. signed Merkle tree root hash (plain bits, 64 bytes)
## Adding new message ## Adding new message

View File

@ -10,7 +10,7 @@ import pathlib
import re import re
import shutil import shutil
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List, TextIO, Tuple from typing import Any, Dict, List, TextIO, Tuple, cast
import click import click
import ed25519 import ed25519
@ -24,9 +24,9 @@ from coin_info import (
Coins, Coins,
_load_builtin_erc20_tokens, _load_builtin_erc20_tokens,
_load_builtin_ethereum_networks, _load_builtin_ethereum_networks,
coin_info,
load_json, load_json,
) )
from merkle_tree import MerkleTree
from trezorlib import protobuf from trezorlib import protobuf
from trezorlib.messages import ( from trezorlib.messages import (
EthereumDefinitionType, EthereumDefinitionType,
@ -677,19 +677,38 @@ def check_definitions_list(
_set_definition_metadata(definition) _set_definition_metadata(definition)
def _load_prepared_definitions(): def _load_prepared_definitions(definitions_file: pathlib.Path) -> tuple[list[dict], list[dict]]:
# set keys for the networks and tokens if not definitions_file.is_file():
# networks.append(cast(Coin, network)) click.ClickException(f"File {definitions_file} with prepared definitions does not exists or is not a file.")
# key=f"eth:{shortcut}",
# token.update( prepared_definitions_data = load_json(definitions_file)
# chain=chain, try:
# chain_id=network["chain_id"], networks_data = prepared_definitions_data["networks"]
# address_bytes=bytes.fromhex(token["address"][2:]), tokens_data = prepared_definitions_data["tokens"]
# shortcut=token["symbol"], except KeyError:
# key=f"erc20:{chain}:{token['symbol']}", click.ClickException(f"File with prepared definitions is not complete. Whole \"networks\" and/or \"tokens\" section are missing.")
# )
pass networks: Coins = []
for network_data in networks_data:
network_data.update(
chain_id=str(network_data["chain_id"]),
key=f"eth:{network_data['shortcut']}",
)
networks.append(cast(Coin, network_data))
tokens: Coins = []
for token in tokens_data:
token.update(
chain_id=str(token["chain_id"]),
address=token["address"].lower(),
address_bytes=bytes.fromhex(token["address"][2:]),
symbol=token["shortcut"],
key=f"erc20:{token['chain']}:{token['shortcut']}",
)
tokens.append(cast(Coin, token))
return networks, tokens
# ====== coindefs generators ====== # ====== coindefs generators ======
@ -723,7 +742,10 @@ def serialize_eth_info(
buf = io.BytesIO() buf = io.BytesIO()
protobuf.dump_message(buf, info) protobuf.dump_message(buf, info)
ser += buf.getvalue() msg = buf.getvalue()
# write the length of encoded protobuf message
ser += len(msg).to_bytes(2, "big")
ser += msg
return ser return ser
@ -930,9 +952,6 @@ def prepare_definitions(
f.write("\n") f.write("\n")
# TODO: separate function to generate built-in defs???
@cli.command() @cli.command()
@click.option( @click.option(
"-o", "-o",
@ -946,7 +965,14 @@ def prepare_definitions(
type=click.File(mode="r"), type=click.File(mode="r"),
help="Private key (text, hex formated) to use to sign data. Could be also loaded from `PRIVATE_KEY` env variable. Provided file is preffered over env variable.", help="Private key (text, hex formated) to use to sign data. Could be also loaded from `PRIVATE_KEY` env variable. Provided file is preffered over env variable.",
) )
def coindefs(outdir: pathlib.Path, privatekey: TextIO) -> None: @click.option(
"-d",
"--deffile",
type=click.Path(resolve_path=True, dir_okay=False, path_type=pathlib.Path),
default="./definitions-latest.json",
help="File where the prepared definitions are saved in json format."
)
def sign_definitions(outdir: pathlib.Path, privatekey: TextIO, deffile: pathlib.Path) -> None:
"""Generate signed Ethereum definitions for python-trezor and others.""" """Generate signed Ethereum definitions for python-trezor and others."""
hex_key = None hex_key = None
if privatekey is None: if privatekey is None:
@ -962,68 +988,92 @@ def coindefs(outdir: pathlib.Path, privatekey: TextIO) -> None:
sign_key = ed25519.SigningKey(ed25519.from_ascii(hex_key, encoding="hex")) sign_key = ed25519.SigningKey(ed25519.from_ascii(hex_key, encoding="hex"))
def save_definition(directory: pathlib.Path, keys: list[str], data: bytes): def save_definition(directory: pathlib.Path, keys: list[str], data: bytes):
complete_filename = "_".join(keys) + ".dat" complete_file_path = directory / ("_".join(keys) + ".dat")
with open(directory / complete_filename, mode="wb+") as f:
if complete_file_path.exists():
raise click.ClickException(
f"Definition \"{complete_file_path}\" already generated - attempt to generate another definition."
)
directory.mkdir(parents=True, exist_ok=True)
with open(complete_file_path, mode="wb+") as f:
f.write(data) f.write(data)
def generate_token_defs(tokens: Coins, path: pathlib.Path): def generate_token_defs(tokens: Coins):
for token in tokens: for token in tokens:
if token["address"] is None: if token["address"] is None or token["chain_id"] is None:
continue continue
# generate definition of the token # save token definition
keys = ["token", token["address"][2:].lower()] save_definition(
ser = serialize_eth_info( outdir / "by_chain_id" / token["chain_id"],
eth_info_from_dict(token, EthereumTokenInfo), ["token", token["address"][2:].lower()],
EthereumDefinitionType.TOKEN, token["serialized"],
) )
save_definition(path, keys, ser + sign_data(sign_key, ser))
def generate_network_def(net: Coin, tokens: Coins): def generate_network_def(network: Coin):
if net["chain_id"] is None: if network["chain_id"] is None:
return return
# create path for networks identified by chain ids # create path for networks identified by chain and slip44 ids
network_dir = outdir / "by_chain_id" / str(net["chain_id"]) network_dir = outdir / "by_chain_id" / network["chain_id"]
slip44_dir = outdir / "by_slip44" / str(network["slip44"])
# save network definition
save_definition(network_dir, ["network"], network["serialized"])
try: try:
network_dir.mkdir(parents=True) # TODO: this way only the first network with given slip is saved - save other networks??
except FileExistsError: save_definition(slip44_dir, ["network"], network["serialized"])
raise click.ClickException( except click.ClickException:
f"Network with chain ID {net['chain_id']} already exists - attempt to generate defs for network \"{net['name']}\" ({net['shortcut']})." pass
)
# generate definition of the network # load prepared definitions
keys = ["network"] networks, tokens = _load_prepared_definitions(deffile)
ser = serialize_eth_info(
eth_info_from_dict(net, EthereumNetworkInfo), EthereumDefinitionType.NETWORK
)
complete_data = ser + sign_data(sign_key, ser)
save_definition(network_dir, keys, complete_data)
# generate tokens for the network
generate_token_defs(tokens, network_dir)
# create path for networks identified by slip44 ids
slip44_dir = outdir / "by_slip44" / str(net["slip44"])
if not slip44_dir.exists():
slip44_dir.mkdir(parents=True)
# TODO: save only first network??
save_definition(slip44_dir, keys, complete_data)
# clear defs directory # clear defs directory
if outdir.exists(): if outdir.exists():
shutil.rmtree(outdir) shutil.rmtree(outdir)
outdir.mkdir(parents=True) outdir.mkdir(parents=True)
all_coins = coin_info.coin_info() # serialize definitions
definitions_by_serialization: dict[bytes, dict] = dict()
for network in networks:
ser = serialize_eth_info(
eth_info_from_dict(network, EthereumNetworkInfo), EthereumDefinitionType.NETWORK
)
network["serialized"] = ser
definitions_by_serialization[ser] = network
for token in tokens:
ser = serialize_eth_info(
eth_info_from_dict(token, EthereumTokenInfo), EthereumDefinitionType.TOKEN
)
token["serialized"] = ser
definitions_by_serialization[ser] = token
# group tokens by their chain_id # build Merkle tree
token_buckets: CoinBuckets = defaultdict(list) mt = MerkleTree(
for token in all_coins.erc20: [network["serialized"] for network in networks] +
token_buckets[token["chain_id"]].append(token) [token["serialized"] for token in tokens]
)
for network in all_coins.eth: # sign tree root hash
generate_network_def(network, token_buckets[network["chain_id"]]) signed_root_hash = sign_data(sign_key, mt.get_root_hash())
# update definitions
for ser, proof in mt.get_proofs().items():
definition = definitions_by_serialization[ser]
# append number of hashes in proof
definition["serialized"] += len(proof).to_bytes(1, "big")
# append proof itself
for p in proof:
definition["serialized"] += p
# append signed tree root hash
definition["serialized"] += signed_root_hash
for network in networks:
generate_network_def(network)
generate_token_defs(tokens)
if __name__ == "__main__": if __name__ == "__main__":

79
common/tools/merkle_tree.py Executable file
View File

@ -0,0 +1,79 @@
from typing import Optional, Union
try:
from trezor.crypto.hashlib import sha256
except ImportError:
from hashlib import sha256
class Node():
"""
Single node of Merkle tree.
"""
def __init__(self: "Node", left: Union[bytes, "Node"], right: Optional["Node"] = None) -> None:
self.is_leaf = (left is None) != (right is None) # XOR
if self.is_leaf:
self.raw_value = left
self.hash = None
self.left_child = left
self.right_child = right
self.proof_list: list[bytes] = []
def compute_hash(self) -> bytes:
if not self.hash:
if self.is_leaf:
self.hash = sha256(b"\x00" + self.left_child).digest()
else:
left_hash = self.left_child.compute_hash()
right_hash = self.right_child.compute_hash()
hash_a = min(left_hash, right_hash)
hash_b = max(left_hash, right_hash)
self.hash = sha256(b"\x01" + hash_a + hash_b).digest()
# distribute proof
self.left_child.add_to_proof(right_hash)
self.right_child.add_to_proof(left_hash)
return self.hash
def add_to_proof(self, proof: bytes) -> None:
self.proof_list.append(proof)
if not self.is_leaf:
self.left_child.add_to_proof(proof)
self.right_child.add_to_proof(proof)
class MerkleTree():
"""
Simple Merkle tree that implements the building of Merkle tree itself and generate proofs
for leaf nodes.
"""
def __init__(self, values: list[bytes]) -> None:
self.leaves = [Node(v) for v in values]
# build the tree
actual_level = [n for n in self.leaves]
while len(actual_level) > 1:
# build one level of the tree
next_level = []
while len(actual_level) // 2:
left_node = actual_level.pop(0)
right_node = actual_level.pop(0)
next_level.append(Node(left_node, right_node))
if len(actual_level) == 1:
# odd number of nodes on actual level so last node will be "joined" on another level
next_level.append(actual_level.pop(0))
# switch levels and continue
actual_level = next_level
# set root and compute hash
self.root_node = actual_level[0]
self.root_node.compute_hash()
def get_proofs(self) -> dict[bytes, list[bytes]]:
return {n.raw_value: n.proof_list for n in self.leaves}
def get_root_hash(self) -> bytes:
return self.root_node.hash

View File

@ -27,15 +27,31 @@ if __debug__:
class EthereumDefinitionParser: class EthereumDefinitionParser:
def __init__(self, definition_bytes: bytes) -> None: def __init__(self, definition_bytes: bytes) -> None:
if len(definition_bytes) <= (8 + 1 + 4 + 64): actual_position = 0
raise wire.DataError("Received Ethereum definition is probably malformed (too few data).")
self.format_version: str = definition_bytes[:8].rstrip(b'\0').decode("utf-8") try:
# prefix
self.format_version = definition_bytes[:8].rstrip(b'\0').decode("utf-8")
self.definition_type: int = definition_bytes[8] self.definition_type: int = definition_bytes[8]
self.data_version: int = int.from_bytes(definition_bytes[9:13], 'big') self.data_version = int.from_bytes(definition_bytes[9:13], 'big')
self.clean_payload = definition_bytes[13:-64] self.payload_length_in_bytes = int.from_bytes(definition_bytes[13:15], 'big')
self.payload = definition_bytes[:-64] actual_position += 8 + 1 + 4 + 2
self.signature = definition_bytes[-64:]
# payload
self.payload = definition_bytes[actual_position:(actual_position + self.payload_length_in_bytes)]
self.payload_with_prefix = definition_bytes[:(actual_position + self.payload_length_in_bytes)]
actual_position += self.payload_length_in_bytes
# suffix - Merkle tree proof and signed root hash
self.proof_length: int = definition_bytes[actual_position]
actual_position += 1
self.proof: list[bytes] = []
for _ in range(self.proof_length):
self.proof.append(definition_bytes[actual_position:(actual_position + 32)])
actual_position += 32
self.signed_tree_root = definition_bytes[actual_position:(actual_position + 64)]
except IndexError:
raise wire.DataError("Invalid Ethereum definition.")
def decode_definition( def decode_definition(
@ -56,19 +72,32 @@ def decode_definition(
if parsed_definition.data_version < MIN_DATA_VERSION: if parsed_definition.data_version < MIN_DATA_VERSION:
raise wire.DataError("Used Ethereum definition data version too low.") raise wire.DataError("Used Ethereum definition data version too low.")
# at the end verify the signature # at the end verify the signature - compute Merkle tree root hash using provided leaf data and proof
if not ed25519.verify(DEFINITIONS_PUBLIC_KEY, parsed_definition.signature, parsed_definition.payload): def compute_mt_root_hash(data: bytes, proof: list[bytes]) -> bytes:
from trezor.crypto.hashlib import sha256
hash = sha256(b"\x00" + data).digest()
for p in proof:
hash_a = min(hash, p)
hash_b = max(hash, p)
hash = sha256(b"\x01" + hash_a + hash_b).digest()
return hash
# verify Merkle proof
root_hash = compute_mt_root_hash(parsed_definition.payload_with_prefix, parsed_definition.proof)
if not ed25519.verify(DEFINITIONS_PUBLIC_KEY, parsed_definition.signed_tree_root, root_hash):
error_msg = wire.DataError("Ethereum definition signature is invalid.") error_msg = wire.DataError("Ethereum definition signature is invalid.")
if __debug__: if __debug__:
# check against dev key # check against dev key
if not ed25519.verify(DEFINITIONS_DEV_PUBLIC_KEY, parsed_definition.signature, parsed_definition.payload): if not ed25519.verify(DEFINITIONS_DEV_PUBLIC_KEY, parsed_definition.signed_tree_root, root_hash):
raise error_msg raise error_msg
else: else:
raise error_msg raise error_msg
# decode it if it's OK # decode it if it's OK
if expected_type == EthereumDefinitionType.NETWORK: if expected_type == EthereumDefinitionType.NETWORK:
info = protobuf.decode(parsed_definition.clean_payload, EthereumNetworkInfo, True) info = protobuf.decode(parsed_definition.payload, EthereumNetworkInfo, True)
# TODO: temporarily convert to internal class # TODO: temporarily convert to internal class
if info is not None: if info is not None:
@ -81,7 +110,7 @@ def decode_definition(
rskip60=info.rskip60 rskip60=info.rskip60
) )
else: else:
info = protobuf.decode(parsed_definition.clean_payload, EthereumTokenInfo, True) info = protobuf.decode(parsed_definition.payload, EthereumTokenInfo, True)
# TODO: temporarily convert to internal class # TODO: temporarily convert to internal class
if info is not None: if info is not None:

View File

@ -12,7 +12,7 @@ if TYPE_CHECKING:
from trezor.messages import EthereumSignTx, EthereumTxAck from trezor.messages import EthereumSignTx, EthereumTxAck
from trezor.wire import Context from trezor.wire import Context
from .keychain import EthereumSignTxAny from .keychain import MsgInKeychainChainIdDefs
from . import tokens, definitions from . import tokens, definitions
@ -100,7 +100,7 @@ async def sign_tx(
async def handle_erc20( async def handle_erc20(
ctx: Context, msg: EthereumSignTxAny, token_dict: dict[bytes, tokens.TokenInfo] ctx: Context, msg: MsgInKeychainChainIdDefs, token_dict: dict[bytes, tokens.TokenInfo]
) -> tuple[tokens.TokenInfo | None, bytes, bytes, int]: ) -> tuple[tokens.TokenInfo | None, bytes, bytes, int]:
from .layout import require_confirm_unknown_token from .layout import require_confirm_unknown_token
from . import tokens from . import tokens
@ -185,7 +185,7 @@ def _sign_digest(
return req return req
def check_common_fields(msg: EthereumSignTxAny) -> None: def check_common_fields(msg: MsgInKeychainChainIdDefs) -> None:
data_length = msg.data_length # local_cache_attribute data_length = msg.data_length # local_cache_attribute
if data_length > 0: if data_length > 0:

View File

@ -35,7 +35,6 @@ class TokenInfo:
UNKNOWN_TOKEN = TokenInfo("Wei UNKN", 0, b"", 0) UNKNOWN_TOKEN = TokenInfo("Wei UNKN", 0, b"", 0)
# TODO: delete completely
def token_by_chain_address(chain_id: int, address: bytes) -> TokenInfo: def token_by_chain_address(chain_id: int, address: bytes) -> TokenInfo:
for addr, symbol, decimal in _token_iterator(chain_id): for addr, symbol, decimal in _token_iterator(chain_id):
if address == addr: if address == addr:

View File

@ -44,7 +44,6 @@ class TokenInfo:
UNKNOWN_TOKEN = TokenInfo("Wei UNKN", 0, b"", 0) UNKNOWN_TOKEN = TokenInfo("Wei UNKN", 0, b"", 0)
# TODO: delete completely
def token_by_chain_address(chain_id: int, address: bytes) -> TokenInfo: def token_by_chain_address(chain_id: int, address: bytes) -> TokenInfo:
for addr, symbol, decimal in _token_iterator(chain_id): for addr, symbol, decimal in _token_iterator(chain_id):
if address == addr: if address == addr:

View File

@ -1,12 +1,13 @@
from typing import Iterator
from trezor.utils import ensure from trezor.utils import ensure
from ubinascii import unhexlify # noqa: F401 from ubinascii import hexlify, unhexlify # noqa: F401
from trezor import messages from trezor import messages
from apps.ethereum import networks, tokens from apps.ethereum import networks, tokens
EXPECTED_FORMAT_VERSION = 1 EXPECTED_FORMAT_VERSION = 1
EXPECTED_DATA_VERSION = 1657791892 # unix epoch time Thu Jul 14 2022 09:44:52 GMT+0000 EXPECTED_DATA_VERSION = 1663054984 # unix epoch time
class InfoWithDefinition(): class InfoWithDefinition():
def __init__(self, definition, info): def __init__(self, definition, info):
@ -14,12 +15,12 @@ class InfoWithDefinition():
self.info = info self.info = info
# definitions created by `common/tools/cointool.py` and copied here # definitions created by `common/tools/ethereum_definitions.py` and copied here
NETWORKS = { NETWORKS = {
# chain_id: network info with encoded definition # chain_id: network info with encoded definition
# Ethereum # Ethereum
1: InfoWithDefinition( 1: InfoWithDefinition(
definition=unhexlify("74727a64310000000062d6384d0801103c1a034554482208457468657265756d28003342fb0073eb26285e8b50f402a346fee0d37b98721c516d13864043e54d7e33e31d82004024495138bc72b946b080f5d319ea41c8ba53aaffbf7988419f860e"), definition=None, # built-in definitions are not encoded
info=networks.NetworkInfo( info=networks.NetworkInfo(
chain_id=1, chain_id=1,
slip44=60, slip44=60,
@ -28,20 +29,20 @@ NETWORKS = {
rskip60=False, rskip60=False,
), ),
), ),
# Expanse # Rinkeby
2: InfoWithDefinition( 4: InfoWithDefinition(
definition=unhexlify("74727a64310000000062d6384d080210281a03455850220f457870616e7365204e6574776f726b2800b5f8aba1a056398b340fc16d66ca845db1cbd258cb1a560b6d1d227655ff1c88cd785a3005c31cc2a1da3b20edc69c09d470d0ebabb19f52e3cfb7139dac9104"), definition=unhexlify("74727a643100000000632034880015080410011a047452494e220752696e6b65627928000e8cc47ed4e657d9a9b98e1dd02164320c54a9724e17f91d1d79f6760169582c98ec70ca6f4e94d27e574175c59d2ae04e0cd30b65fb19acd8d2c5fb90bcb7db96f6102e4182c0cef5f412ac3c5fa94f9505b4df2633a0f7bdffa309588d722415624adeb8f329b1572ff9dfc81fbc86e61f1fcb2369f51ba85ea765c908ac254ba996f842a6277583f8d02f149c78bc0eeb8f3d41240403f85785dc3a3925ea768d76aae12342c8a24de223c1ea75e5f07f6b94b8f22189413631eed3c9a362b4501f68b645aa487b9d159a8161404a218507641453ebf045cec56710bb7d873e102777695b56903766e1af16f95576ec4f41874bdaf80cec02ee067d30e721515564d4f30fa74a6c61eb784ea65cc881ead7af2ffac02d5bf1fe1a756918fe37b74828a24b640025cd79443ada60063e3034444fc49ed6055dbba6a09fa4484c42cb85abb49103dc8c781c8f190c4632e2dec30081770448021313955dbb49e8a02fd49b34d030280452fe0a5c3bcba4958bc287c67e12519be4f4aec7ab0c8e574e53a663f635f75508f23d92c77b2147f29feb79c38d0f793fba295aae605c7e8226523edefc6ad1eefe088e5b8376028bf90116ece4fb876510b4ae1c89686dbcaacbbac8225baba429ca376fafac50f4bd1ff4ce1c61dd53318d0718bf513ea6f770cce81e07a653622e4dbd03bdaa570bfe43219eb0d4fab725c9a8da04"),
info=networks.NetworkInfo( info=networks.NetworkInfo(
chain_id=2, chain_id=4,
slip44=40, slip44=1,
shortcut="EXP", shortcut="tRIN",
name="Expanse Network", name="Rinkeby",
rskip60=False, rskip60=False,
), ),
), ),
# Ubiq # Ubiq
8: InfoWithDefinition( 8: InfoWithDefinition(
definition=unhexlify("74727a64310000000062d6384d0808106c1a035542512204556269712800505661237b6e6c5ff7a7b4b6e9f5d6dbf70055507bb6ca48e1260e76a2a01a1788038d80588d643da576183842d6367d7f1c9fefc15d56fd8b15ee8f1165bb0c"), definition=unhexlify("74727a6431000000006320348800110808106c1a0355425122045562697128000e5641d82e3622b4e6addd4354efd933cf15947d1d608a60d324d1156b5a4999f70c41beb85bd866aa3059123447dfeef2e1b6c009b66ac8d04ebbca854ad30049edbbb2fbfda3bfedc6fdb4a76f1db8a4f210bd89d3c3ec1761157b0ec2b13e2f624adeb8f329b1572ff9dfc81fbc86e61f1fcb2369f51ba85ea765c908ac254ba996f842a6277583f8d02f149c78bc0eeb8f3d41240403f85785dc3a3925ea768d76aae12342c8a24de223c1ea75e5f07f6b94b8f22189413631eed3c9a362b4501f68b645aa487b9d159a8161404a218507641453ebf045cec56710bb7d873e102777695b56903766e1af16f95576ec4f41874bdaf80cec02ee067d30e721515564d4f30fa74a6c61eb784ea65cc881ead7af2ffac02d5bf1fe1a756918fe37b74828a24b640025cd79443ada60063e3034444fc49ed6055dbba6a09fa4484c42cb85abb49103dc8c781c8f190c4632e2dec30081770448021313955dbb49e8a02fd49b34d030280452fe0a5c3bcba4958bc287c67e12519be4f4aec7ab0c8e574e53a663f635f75508f23d92c77b2147f29feb79c38d0f793fba295aae605c7e8226523edefc6ad1eefe088e5b8376028bf90116ece4fb876510b4ae1c89686dbcaacbbac8225baba429ca376fafac50f4bd1ff4ce1c61dd53318d0718bf513ea6f770cce81e07a653622e4dbd03bdaa570bfe43219eb0d4fab725c9a8da04"),
info=networks.NetworkInfo( info=networks.NetworkInfo(
chain_id=8, chain_id=8,
slip44=108, slip44=108,
@ -52,7 +53,7 @@ NETWORKS = {
), ),
# Ethereum Classic # Ethereum Classic
61: InfoWithDefinition( 61: InfoWithDefinition(
definition=unhexlify("74727a64310000000062d6384d083d103d1a034554432210457468657265756d20436c61737369632800095cb5be721429440fd9856d7c35acc7751c9cb1d006189a78f5693654b36c39bb4ab5add8d5cd6af3c345a8aa20307f79567c78c4f5940e3f16221a0c5bd30f"), definition=unhexlify("74727a64310000000063203488001d083d103d1a034554432210457468657265756d20436c617373696328000e6b891a57fe4c38c54b475f22f0d9242dd8ddab0b4f360bd86e37e2e8b79de5ef29237436351f7bc924cd110716b5adde7c28c03d76ac83b091dbce1b5d7d0edbddb221bd894806f7ea1b195443176e06830a83c0204e33f19c51d2fccc3a9f80ac2cca38822db998ddf76778dada240d39b3c6193c6335d7c693dea90d19a41f86855375c2f48c18cdc012ccac771aa316d776c8721c2b1f6d5980808337dfdae13b5be07e3cbc3526119b88c5eb44be0b1dab1094a5ec5215b47daf91736d16501f68b645aa487b9d159a8161404a218507641453ebf045cec56710bb7d873e102777695b56903766e1af16f95576ec4f41874bdaf80cec02ee067d30e721515564d4f30fa74a6c61eb784ea65cc881ead7af2ffac02d5bf1fe1a756918fe37b74828a24b640025cd79443ada60063e3034444fc49ed6055dbba6a09fa4484c42cb85abb49103dc8c781c8f190c4632e2dec30081770448021313955dbb49e8a02fd49b34d030280452fe0a5c3bcba4958bc287c67e12519be4f4aec7ab0c8e574e53a663f635f75508f23d92c77b2147f29feb79c38d0f793fba295aae605c7e8226523edefc6ad1eefe088e5b8376028bf90116ece4fb876510b4ae1c89686dbcaacbbac8225baba429ca376fafac50f4bd1ff4ce1c61dd53318d0718bf513ea6f770cce81e07a653622e4dbd03bdaa570bfe43219eb0d4fab725c9a8da04"),
info=networks.NetworkInfo( info=networks.NetworkInfo(
chain_id=61, chain_id=61,
slip44=61, slip44=61,
@ -62,21 +63,23 @@ NETWORKS = {
), ),
), ),
} }
SLIP44_TO_CHAIN_ID_MAP = { SLIP44_TO_CHAIN_ID_MAP = {
40: [2], 1: [4],
60: [1], 60: [1],
61: [61], 61: [61],
108: [8], 108: [8],
} }
# definitions created by `common/tools/cointool.py` and copied here # definitions created by `common/tools/ethereum_definitions.py` and copied here
TOKENS = { TOKENS = {
# chain_id: { address: token info with encoded definition, address: token info with encoded definition,... } # chain_id: { address: token info with encoded definition, address: token info with encoded definition,... }
1: { 1: {
# AAVE # AAVE
"7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9": InfoWithDefinition( "7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9": InfoWithDefinition(
definition=unhexlify("74727a64310000000162d6384d0a044141564510121a147fc66500c84a76ad7e9c93437bfc5ac33e2ddae920012a04416176652d5458fe6726f8dfad0af3d1bd0959199da25dba4f429c14517f54acdd4b4117b676f7467bb34fe9a64dbd95d1a875085774df7b30fdf09f0b33f4e52b2d900b"), definition=None, # built-in definitions are not encoded
info=tokens.TokenInfo( info=tokens.TokenInfo(
symbol="AAVE", symbol="AAVE",
decimals=18, decimals=18,
@ -84,16 +87,26 @@ TOKENS = {
chain_id=1, chain_id=1,
), ),
), ),
}, # TrueAUD
61: { "00006100f7090010005f1bd7ae6122c3c2cf0090": InfoWithDefinition(
# BEC definition=unhexlify("74727a6431000000016320348800290a045441554410121a1400006100f7090010005f1bd7ae6122c3c2cf009020012a07547275654155440e310dad13f7d3012903a9a457134c9f38c62c04370cb92c7a528838e30a032dffbceeaa2aa849e590c4e6dbc69b0ea5359f3527b95b56ab59a33dc584105b35ea7c06afc296cc1c1e58cc3d6b461631c4c770b9409837ab3d29bc1b666fb9cf5245c4c218b0e9521c185d102f596905ba860e6f56a0a8b394f943855c74eea6fcac87210a9988ac02803f4cc61cf78e7e2409175a75f4f3a82eb84b1f2d1ea8177d5dccd62949d80d7942105e22a452be01859fe816736e803b120fb9bcc0c1117180dbda19e1ad1aafb9b9f1555c75275820bf7c1e568bcb265bdc4dfdae0511782026e11a151f6894d11128327c8c42958c9ae900af970fec13a11ffdeba6ac10733ca55a906142e0b9130312e8e85606108612581aca9087c452f38f14185db74828a24b640025cd79443ada60063e3034444fc49ed6055dbba6a09fa4484c42cb85abb49103dc8c781c8f190c4632e2dec30081770448021313955dbb49e8a02fd49b34d030280452fe0a5c3bcba4958bc287c67e12519be4f4aec7ab0c8e574e53a663f635f75508f23d92c77b2147f29feb79c38d0f793fba295aae605c7e8226523edefc6ad1eefe088e5b8376028bf90116ece4fb876510b4ae1c89686dbcaacbbac8225baba429ca376fafac50f4bd1ff4ce1c61dd53318d0718bf513ea6f770cce81e07a653622e4dbd03bdaa570bfe43219eb0d4fab725c9a8da04"),
"085fb4f24031eaedbc2b611aa528f22343eb52db": InfoWithDefinition(
definition=unhexlify("74727a64310000000162d68cd70a0342454310081a14085fb4f24031eaedbc2b611aa528f22343eb52db203d2a03424543194264a2b334a93b8592997d311d65f1e0840c150eab051fd93cce8997d5200705e5d256abfcdd3c7b68fc5c72ebe6e32c5205d8f60c47d0986d313a4a92c80a"),
info=tokens.TokenInfo( info=tokens.TokenInfo(
symbol="BEC", symbol="TAUD",
decimals=8, decimals=18,
address=unhexlify("085fb4f24031eaedbc2b611aa528f22343eb52db"), address=unhexlify("00006100f7090010005f1bd7ae6122c3c2cf0090"),
chain_id=61, chain_id=1,
),
),
},
4: {
# Karma Token
"275a5b346599b56917e7b1c9de019dcf9ead861a": InfoWithDefinition(
definition=unhexlify("74727a64310000000163203488002b0a024b4310121a14275a5b346599b56917e7b1c9de019dcf9ead861a20042a0b4b61726d6120546f6b656e0e2b3cb176ff5a2cf431620c1a7eee9aa297f5de36d29ae6d423166cf7391e41c5826c57f30b11421a4bf10f336f12050f6d959e02bfb17a8ce7ae15087d4f083124c0cebed2ce45b15b2608b1a8f0ee443e8c4f33111d880a6a3c09a77c627f82d68b62a1bd39975b2a2c86f196b9a3dcb62bdc3554fbf85b75331bc0d39f23a46f5ed91f208757d1136bb20b3618294fbfb0a826e9c09e392fe8109181bc6c28cad78db1987947f461bfc1042b88a91d6d61297d0cf194dfeea981b4515c2ed09dc2966671f5c715c64ceb25e53e1df3c7234e3e0ddf0dcd54d40fde0c51903685f9dc7fa69c71184f17af852e74490ea7286e89a0aa4770629664f7dd8eab8c4e009ff4c24682f85f7e01d4e10ae5c06212d5a4f43bac2b4f0e79383666ef12054ddbf757809aa6b446d65f7fd1bdd76fb1d7770398bd17af50635027e680801d244bd7b4f14c57edc3cd961722315e076120bf1d35db8520edb812bfbb5bab8ff57cc2dc1b3d1f9d95b33dba5d759aef1123f2ef346b6328973fba204fd745e644c8e492f9a76c0019b2cf21715fba682b46b9c58013e0b0927e5272c808a67e8226523edefc6ad1eefe088e5b8376028bf90116ece4fb876510b4ae1c89686dbcaacbbac8225baba429ca376fafac50f4bd1ff4ce1c61dd53318d0718bf513ea6f770cce81e07a653622e4dbd03bdaa570bfe43219eb0d4fab725c9a8da04"),
info=tokens.TokenInfo(
symbol="KC",
decimals=18,
address=unhexlify("275a5b346599b56917e7b1c9de019dcf9ead861a"),
chain_id=4,
), ),
), ),
}, },
@ -187,3 +200,22 @@ def get_ethereum_encoded_definition(chain_id: int | None = None, slip44: int | N
encoded_network=get_ethereum_encoded_network_definition(chain_id, slip44), encoded_network=get_ethereum_encoded_network_definition(chain_id, slip44),
encoded_token=None, # TODO encoded_token=None, # TODO
) )
def builtin_networks_iterator() -> Iterator[networks.NetworkInfo]:
"""Mockup function replaces original function from core/src/apps/ethereum/networks.py used to get built-in network definitions."""
for _, network in NETWORKS.items():
if network.definition is None:
yield network.info
def builtin_token_by_chain_address(chain_id: int, address: bytes) -> tokens.TokenInfo:
"""Mockup function replaces original function from core/src/apps/ethereum/tokens.py used to get built-in token definitions."""
address_str = hexlify(address).decode('hex')
try:
if TOKENS[chain_id][address_str].definition is None:
return TOKENS[chain_id][address_str].info
except KeyError:
pass
return tokens.UNKNOWN_TOKEN

View File

@ -23,26 +23,38 @@ if not utils.BITCOIN_ONLY:
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestEthereumDefinitionParser(unittest.TestCase): class TestEthereumDefinitionParser(unittest.TestCase):
def setUp(self): def setUp(self):
# prefix
self.format_version = b'trzd1' + b'\x00' * 3 self.format_version = b'trzd1' + b'\x00' * 3
self.definition_type = b'\x02' self.definition_type = b'\x01'
self.data_version = b'\x00\x00\x00\x03' self.data_version = b'\x00\x00\x00\x02'
self.clean_payload = b'\x04' # optional length self.payload_length_in_bytes = b'\x00\x03'
self.payload = self.format_version + self.definition_type + self.data_version + self.clean_payload self.prefix = self.format_version + self.definition_type + self.data_version + self.payload_length_in_bytes
self.signature = b'\x00' * 63 + b'\x05'
self.definition = self.payload + self.signature # payload
self.payload = b'\x00\x00\x04' # optional length
self.payload_with_prefix = self.prefix + self.payload
# suffix - Merkle tree proof and signed root hash
self.proof_length = b'\x01'
self.proof = b'\x00' * 31 + b'\x06'
self.signed_tree_root = b'\x00' * 63 + b'\x07'
self.definition = self.payload_with_prefix + self.proof_length + self.proof + self.signed_tree_root
def test_short_message(self): def test_short_message(self):
with self.assertRaises(wire.DataError): with self.assertRaises(wire.DataError):
dfs.EthereumDefinitionParser(b'\x00' * (len(self.definition) - 1)) dfs.EthereumDefinitionParser(b'\x00')
def test_ok_message(self): def test_ok_message(self):
parser = dfs.EthereumDefinitionParser(self.definition) parser = dfs.EthereumDefinitionParser(self.definition)
self.assertEqual(parser.format_version, self.format_version.rstrip(b'\0').decode("utf-8")) self.assertEqual(parser.format_version, self.format_version.rstrip(b'\0').decode("utf-8"))
self.assertEqual(parser.definition_type, int.from_bytes(self.definition_type, 'big')) self.assertEqual(parser.definition_type, int.from_bytes(self.definition_type, 'big'))
self.assertEqual(parser.data_version, int.from_bytes(self.data_version, 'big')) self.assertEqual(parser.data_version, int.from_bytes(self.data_version, 'big'))
self.assertEqual(parser.clean_payload, self.clean_payload) self.assertEqual(parser.payload_length_in_bytes, int.from_bytes(self.payload_length_in_bytes, 'big'))
self.assertEqual(parser.payload, self.payload) self.assertEqual(parser.payload, self.payload)
self.assertEqual(parser.signature, self.signature) self.assertEqual(parser.payload_with_prefix, self.payload_with_prefix)
self.assertEqual(parser.proof_length, int.from_bytes(self.proof_length, 'big'))
self.assertEqual(parser.proof, [self.proof])
self.assertEqual(parser.signed_tree_root, self.signed_tree_root)
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
@ -53,38 +65,38 @@ class TestDecodeDefinition(unittest.TestCase):
# successful decode network # successful decode network
def test_network_definition(self): def test_network_definition(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
self.assertEqual(dfs.decode_definition(eth_network.definition, EthereumDefinitionType.NETWORK), eth_network.info) self.assertEqual(dfs.decode_definition(rinkeby_network.definition, EthereumDefinitionType.NETWORK), rinkeby_network.info)
# successful decode token # successful decode token
def test_token_definition(self): def test_token_definition(self):
# AAVE # Karma Token
eth_token = get_ethereum_token_info_with_definition(chain_id=1, token_address="7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9") kc_token = get_ethereum_token_info_with_definition(chain_id=4)
self.assertEqual(dfs.decode_definition(eth_token.definition, EthereumDefinitionType.TOKEN), eth_token.info) self.assertEqual(dfs.decode_definition(kc_token.definition, EthereumDefinitionType.TOKEN), kc_token.info)
def test_invalid_data(self): def test_invalid_data(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
invalid_dataset = [] invalid_dataset = []
# mangle signature # mangle signature
invalid_dataset.append(bytearray(eth_network.definition)) invalid_dataset.append(bytearray(rinkeby_network.definition))
invalid_dataset[-1][-1] += 1 invalid_dataset[-1][-1] += 1
# mangle payload # mangle payload
invalid_dataset.append(bytearray(eth_network.definition)) invalid_dataset.append(bytearray(rinkeby_network.definition))
invalid_dataset[-1][-65] += 1 invalid_dataset[-1][16] += 1
# wrong format version # wrong format version
invalid_dataset.append(bytearray(eth_network.definition)) invalid_dataset.append(bytearray(rinkeby_network.definition))
invalid_dataset[-1][:5] = b'trzd2' # change "trzd1" to "trzd2" invalid_dataset[-1][:5] = b'trzd2' # change "trzd1" to "trzd2"
# wrong definition type # wrong definition type
invalid_dataset.append(bytearray(eth_network.definition)) invalid_dataset.append(bytearray(rinkeby_network.definition))
invalid_dataset[-1][8] += 1 invalid_dataset[-1][8] += 1
# wrong data format version # wrong data format version
invalid_dataset.append(bytearray(eth_network.definition)) invalid_dataset.append(bytearray(rinkeby_network.definition))
invalid_dataset[-1][13] += 1 invalid_dataset[-1][13] += 1
for data in invalid_dataset: for data in invalid_dataset:
@ -92,15 +104,17 @@ class TestDecodeDefinition(unittest.TestCase):
dfs.decode_definition(bytes(data), EthereumDefinitionType.NETWORK) dfs.decode_definition(bytes(data), EthereumDefinitionType.NETWORK)
def test_wrong_requested_type(self): def test_wrong_requested_type(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
with self.assertRaises(wire.DataError): with self.assertRaises(wire.DataError):
dfs.decode_definition(eth_network.definition, EthereumDefinitionType.TOKEN) dfs.decode_definition(rinkeby_network.definition, EthereumDefinitionType.TOKEN)
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestGetNetworkDefiniton(unittest.TestCase): class TestGetNetworkDefiniton(unittest.TestCase):
def setUp(self): def setUp(self):
self.addTypeEqualityFunc(networks.NetworkInfo, equalNetworkInfo) self.addTypeEqualityFunc(networks.NetworkInfo, equalNetworkInfo)
# use mockup function for built-in networks
networks._networks_iterator = builtin_networks_iterator
def test_get_network_definition(self): def test_get_network_definition(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) eth_network = get_ethereum_network_info_with_definition(chain_id=1)
@ -125,11 +139,11 @@ class TestGetNetworkDefiniton(unittest.TestCase):
# reference chain_id is used to check the encoded network chain_id - so in case they do not equal # reference chain_id is used to check the encoded network chain_id - so in case they do not equal
# error is raised # error is raised
with self.assertRaises(wire.DataError): with self.assertRaises(wire.DataError):
dfs._get_network_definiton(ubiq_network.definition, ubiq_network.info.chain_id + 1) dfs._get_network_definiton(ubiq_network.definition, ubiq_network.info.chain_id + 9999)
def test_invalid_encoded_definition(self): def test_invalid_encoded_definition(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
definition = bytearray(eth_network.definition) definition = bytearray(rinkeby_network.definition)
# mangle signature - this should have the same effect as it has in "decode_definition" function # mangle signature - this should have the same effect as it has in "decode_definition" function
definition[-1] += 1 definition[-1] += 1
with self.assertRaises(wire.DataError): with self.assertRaises(wire.DataError):
@ -140,41 +154,43 @@ class TestGetNetworkDefiniton(unittest.TestCase):
class TestGetTokenDefiniton(unittest.TestCase): class TestGetTokenDefiniton(unittest.TestCase):
def setUp(self): def setUp(self):
self.addTypeEqualityFunc(tokens.TokenInfo, equalTokenInfo) self.addTypeEqualityFunc(tokens.TokenInfo, equalTokenInfo)
# use mockup function for built-in tokens
tokens.token_by_chain_address = builtin_token_by_chain_address
def test_get_token_definition(self): def test_get_token_definition(self):
eth_token = get_ethereum_token_info_with_definition(chain_id=1) aave_token = get_ethereum_token_info_with_definition(chain_id=1, token_address="7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9")
self.assertEqual(dfs._get_token_definiton(None, eth_token.info.chain_id, eth_token.info.address), eth_token.info) self.assertEqual(dfs._get_token_definiton(None, aave_token.info.chain_id, aave_token.info.address), aave_token.info)
def test_built_in_preference(self): def test_built_in_preference(self):
eth_token = get_ethereum_token_info_with_definition(chain_id=1) aave_token = get_ethereum_token_info_with_definition(chain_id=1, token_address="7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9")
eth_classic_token = get_ethereum_token_info_with_definition(chain_id=61) taud_token = get_ethereum_token_info_with_definition(chain_id=1, token_address="00006100f7090010005f1bd7ae6122c3c2cf0090")
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_token.info.chain_id, eth_token.info.address), eth_token.info) self.assertEqual(dfs._get_token_definiton(taud_token.definition, aave_token.info.chain_id, aave_token.info.address), aave_token.info)
def test_no_built_in(self): def test_no_built_in(self):
eth_classic_token = get_ethereum_token_info_with_definition(chain_id=61) kc_token = get_ethereum_token_info_with_definition(chain_id=4)
# use provided (encoded) definition # use provided (encoded) definition
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_classic_token.info.chain_id, eth_classic_token.info.address), eth_classic_token.info) self.assertEqual(dfs._get_token_definiton(kc_token.definition, kc_token.info.chain_id, kc_token.info.address), kc_token.info)
# here the results should be the same as above # here the results should be the same as above
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, None, eth_classic_token.info.address), eth_classic_token.info) self.assertEqual(dfs._get_token_definiton(kc_token.definition, None, kc_token.info.address), kc_token.info)
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_classic_token.info.chain_id, None), eth_classic_token.info) self.assertEqual(dfs._get_token_definiton(kc_token.definition, kc_token.info.chain_id, None), kc_token.info)
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, None, None), eth_classic_token.info) self.assertEqual(dfs._get_token_definiton(kc_token.definition, None, None), kc_token.info)
# nothing should be found # nothing should be found
self.assertEqual(dfs._get_token_definiton(None, eth_classic_token.info.chain_id, eth_classic_token.info.address), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(None, kc_token.info.chain_id, kc_token.info.address), tokens.UNKNOWN_TOKEN)
self.assertEqual(dfs._get_token_definiton(None, None, eth_classic_token.info.address), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(None, None, kc_token.info.address), tokens.UNKNOWN_TOKEN)
self.assertEqual(dfs._get_token_definiton(None, eth_classic_token.info.chain_id, None), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(None, kc_token.info.chain_id, None), tokens.UNKNOWN_TOKEN)
# reference chain_id and/or token address is used to check the encoded token chain_id/address - so in case they do not equal # reference chain_id and/or token address is used to check the encoded token chain_id/address - so in case they do not equal
# tokens.UNKNOWN_TOKEN is returned # tokens.UNKNOWN_TOKEN is returned
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_classic_token.info.chain_id + 1, eth_classic_token.info.address + b"\x00"), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(kc_token.definition, kc_token.info.chain_id + 1, kc_token.info.address + b"\x00"), tokens.UNKNOWN_TOKEN)
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_classic_token.info.chain_id, eth_classic_token.info.address + b"\x00"), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(kc_token.definition, kc_token.info.chain_id, kc_token.info.address + b"\x00"), tokens.UNKNOWN_TOKEN)
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_classic_token.info.chain_id + 1, eth_classic_token.info.address), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(kc_token.definition, kc_token.info.chain_id + 1, kc_token.info.address), tokens.UNKNOWN_TOKEN)
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, None, eth_classic_token.info.address + b"\x00"), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(kc_token.definition, None, kc_token.info.address + b"\x00"), tokens.UNKNOWN_TOKEN)
self.assertEqual(dfs._get_token_definiton(eth_classic_token.definition, eth_classic_token.info.chain_id + 1, None), tokens.UNKNOWN_TOKEN) self.assertEqual(dfs._get_token_definiton(kc_token.definition, kc_token.info.chain_id + 1, None), tokens.UNKNOWN_TOKEN)
def test_invalid_encoded_definition(self): def test_invalid_encoded_definition(self):
eth_token = get_ethereum_token_info_with_definition(chain_id=1) kc_token = get_ethereum_token_info_with_definition(chain_id=4)
definition = bytearray(eth_token.definition) definition = bytearray(kc_token.definition)
# mangle signature - this should have the same effect as it has in "decode_definition" function # mangle signature - this should have the same effect as it has in "decode_definition" function
definition[-1] += 1 definition[-1] += 1
with self.assertRaises(wire.DataError): with self.assertRaises(wire.DataError):
@ -182,10 +198,13 @@ class TestGetTokenDefiniton(unittest.TestCase):
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestEthereumDefinitons(unittest.TestCase): class TestEthereumDefinitions(unittest.TestCase):
def setUp(self): def setUp(self):
self.addTypeEqualityFunc(networks.NetworkInfo, equalNetworkInfo) self.addTypeEqualityFunc(networks.NetworkInfo, equalNetworkInfo)
self.addTypeEqualityFunc(tokens.TokenInfo, equalTokenInfo) self.addTypeEqualityFunc(tokens.TokenInfo, equalTokenInfo)
# use mockup functions for built-in definitions
networks._networks_iterator = builtin_networks_iterator
tokens.token_by_chain_address = builtin_token_by_chain_address
def get_and_compare_ethereum_definitions( def get_and_compare_ethereum_definitions(
self, self,
@ -208,34 +227,38 @@ class TestEthereumDefinitons(unittest.TestCase):
self.assertDictEqual(definitions.token_dict, ref_token_dict) self.assertDictEqual(definitions.token_dict, ref_token_dict)
def test_get_definitions(self): def test_get_definitions(self):
# built-in
eth_network = get_ethereum_network_info_with_definition(chain_id=1) eth_network = get_ethereum_network_info_with_definition(chain_id=1)
eth_token = get_ethereum_token_info_with_definition(chain_id=1) aave_token = get_ethereum_token_info_with_definition(chain_id=1, token_address="7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9")
# not built-in
rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
kc_token = get_ethereum_token_info_with_definition(chain_id=4)
# these variations should have the same result - successfully load built-in or encoded network/token # these variations should have the same result - successfully load built-in or encoded network/token
calls_params = [ calls_params = [
(None, None, eth_network.info.chain_id, eth_token.info.address), (None, None, eth_network.info.chain_id, aave_token.info.address, eth_network.info, aave_token.info),
(eth_network.definition, None, eth_network.info.chain_id, eth_token.info.address), (rinkeby_network.definition, None, eth_network.info.chain_id, aave_token.info.address, eth_network.info, aave_token.info),
(None, eth_token.definition, eth_network.info.chain_id, eth_token.info.address), (None, kc_token.definition, eth_network.info.chain_id, aave_token.info.address, eth_network.info, aave_token.info),
(eth_network.definition, eth_token.definition, eth_network.info.chain_id, eth_token.info.address), (rinkeby_network.definition, kc_token.definition, eth_network.info.chain_id, aave_token.info.address, eth_network.info, aave_token.info),
(eth_network.definition, eth_token.definition, None, eth_token.info.address), (rinkeby_network.definition, kc_token.definition, None, kc_token.info.address, rinkeby_network.info, kc_token.info),
(eth_network.definition, eth_token.definition, eth_network.info.chain_id, None), (rinkeby_network.definition, kc_token.definition, rinkeby_network.info.chain_id, None, rinkeby_network.info, kc_token.info),
(eth_network.definition, eth_token.definition, None, None), (rinkeby_network.definition, kc_token.definition, None, None, rinkeby_network.info, kc_token.info),
] ]
for params in calls_params: for params in calls_params:
self.get_and_compare_ethereum_definitions(*(params + (eth_network.info, eth_token.info))) self.get_and_compare_ethereum_definitions(*params)
def test_no_network_or_token(self): def test_no_network_or_token(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
eth_token = get_ethereum_token_info_with_definition(chain_id=1) kc_token = get_ethereum_token_info_with_definition(chain_id=4)
calls_params = [ calls_params = [
# without network there should be no token loaded # without network there should be no token loaded
(None, eth_token.definition, None, eth_token.info.address, None, None), (None, kc_token.definition, None, kc_token.info.address, None, None),
(None, eth_token.definition, 0, eth_token.info.address, None, None), # non-existing chain_id (None, kc_token.definition, 0, kc_token.info.address, None, None), # non-existing chain_id
# also without token there should be no token loaded # also without token there should be no token loaded
(eth_network.definition, None, eth_network.info.chain_id, None, eth_network.info, None), (rinkeby_network.definition, None, rinkeby_network.info.chain_id, None, rinkeby_network.info, None),
(eth_network.definition, None, eth_network.info.chain_id, eth_token.info.address + b"\x00", eth_network.info, None), # non-existing token address (rinkeby_network.definition, None, rinkeby_network.info.chain_id, kc_token.info.address + b"\x00", rinkeby_network.info, None), # non-existing token address
] ]
for params in calls_params: for params in calls_params:
self.get_and_compare_ethereum_definitions(*params) self.get_and_compare_ethereum_definitions(*params)
@ -246,6 +269,9 @@ class TestGetDefinitonsFromMsg(unittest.TestCase):
def setUp(self): def setUp(self):
self.addTypeEqualityFunc(networks.NetworkInfo, equalNetworkInfo) self.addTypeEqualityFunc(networks.NetworkInfo, equalNetworkInfo)
self.addTypeEqualityFunc(tokens.TokenInfo, equalTokenInfo) self.addTypeEqualityFunc(tokens.TokenInfo, equalTokenInfo)
# use mockup functions for built-in definitions
networks._networks_iterator = builtin_networks_iterator
tokens.token_by_chain_address = builtin_token_by_chain_address
def get_and_compare_ethereum_definitions( def get_and_compare_ethereum_definitions(
self, self,
@ -265,8 +291,12 @@ class TestGetDefinitonsFromMsg(unittest.TestCase):
self.assertDictEqual(definitions.token_dict, ref_token_dict) self.assertDictEqual(definitions.token_dict, ref_token_dict)
def test_get_definitions_SignTx_messages(self): def test_get_definitions_SignTx_messages(self):
# built-in
eth_network = get_ethereum_network_info_with_definition(chain_id=1) eth_network = get_ethereum_network_info_with_definition(chain_id=1)
eth_token = get_ethereum_token_info_with_definition(chain_id=1) aave_token = get_ethereum_token_info_with_definition(chain_id=1, token_address="7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9")
# not built-in
rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
kc_token = get_ethereum_token_info_with_definition(chain_id=4)
def create_EthereumSignTx_msg(**kwargs): def create_EthereumSignTx_msg(**kwargs):
return EthereumSignTx( return EthereumSignTx(
@ -287,62 +317,106 @@ class TestGetDefinitonsFromMsg(unittest.TestCase):
) )
# both network and token should be loaded # both network and token should be loaded
messages = [ params_set = [
(
create_EthereumSignTx_msg(
chain_id=rinkeby_network.info.chain_id,
to=hexlify(kc_token.info.address),
definitions=EthereumEncodedDefinitions(
encoded_network=rinkeby_network.definition,
encoded_token=kc_token.definition,
),
),
rinkeby_network.info,
kc_token.info,
),
(
create_EthereumSignTx_msg( create_EthereumSignTx_msg(
chain_id=eth_network.info.chain_id, chain_id=eth_network.info.chain_id,
to=hexlify(eth_token.info.address), to=hexlify(aave_token.info.address),
definitions=EthereumEncodedDefinitions(eth_network.definition, eth_token.definition),
), ),
create_EthereumSignTx_msg( eth_network.info,
chain_id=eth_network.info.chain_id, aave_token.info,
to=hexlify(eth_token.info.address),
), ),
(
create_EthereumSignTxEIP1559_msg(
chain_id=rinkeby_network.info.chain_id,
to=hexlify(kc_token.info.address),
definitions=EthereumEncodedDefinitions(
encoded_network=rinkeby_network.definition,
encoded_token=kc_token.definition,
),
),
rinkeby_network.info,
kc_token.info,
),
(
create_EthereumSignTxEIP1559_msg( create_EthereumSignTxEIP1559_msg(
chain_id=eth_network.info.chain_id, chain_id=eth_network.info.chain_id,
to=hexlify(eth_token.info.address), to=hexlify(aave_token.info.address),
definitions=EthereumEncodedDefinitions(eth_network.definition, eth_token.definition),
), ),
create_EthereumSignTxEIP1559_msg( eth_network.info,
chain_id=eth_network.info.chain_id, aave_token.info,
to=hexlify(eth_token.info.address),
), ),
] ]
for msg in messages: for params in params_set:
self.get_and_compare_ethereum_definitions(msg, eth_network.info, eth_token.info) self.get_and_compare_ethereum_definitions(*params)
# missing "to" parameter in messages should lead to no token is loaded if none was provided # missing "to" parameter in messages should lead to no token is loaded if none was provided
messages = [ params_set = [
(
create_EthereumSignTx_msg( create_EthereumSignTx_msg(
chain_id=eth_network.info.chain_id, chain_id=rinkeby_network.info.chain_id,
definitions=EthereumEncodedDefinitions(eth_network.definition, None), definitions=EthereumEncodedDefinitions(
encoded_network=rinkeby_network.definition,
encoded_token=None,
), ),
),
rinkeby_network.info,
None,
),
(
create_EthereumSignTx_msg( create_EthereumSignTx_msg(
chain_id=eth_network.info.chain_id, chain_id=eth_network.info.chain_id,
), ),
create_EthereumSignTxEIP1559_msg( eth_network.info,
chain_id=eth_network.info.chain_id, None,
definitions=EthereumEncodedDefinitions(eth_network.definition, None),
), ),
(
create_EthereumSignTxEIP1559_msg(
chain_id=rinkeby_network.info.chain_id,
definitions=EthereumEncodedDefinitions(
encoded_network=rinkeby_network.definition,
encoded_token=None
),
),
rinkeby_network.info,
None,
),
(
create_EthereumSignTxEIP1559_msg( create_EthereumSignTxEIP1559_msg(
chain_id=eth_network.info.chain_id, chain_id=eth_network.info.chain_id,
), ),
eth_network.info,
None,
),
] ]
for msg in messages: for params in params_set:
self.get_and_compare_ethereum_definitions(msg, eth_network.info, None) self.get_and_compare_ethereum_definitions(*params)
def test_other_messages(self): def test_other_messages(self):
eth_network = get_ethereum_network_info_with_definition(chain_id=1) rinkeby_network = get_ethereum_network_info_with_definition(chain_id=4)
# only network should be loaded # only network should be loaded
messages = [ messages = [
EthereumGetAddress(encoded_network=eth_network.definition), EthereumGetAddress(encoded_network=rinkeby_network.definition),
EthereumGetPublicKey(encoded_network=eth_network.definition), EthereumGetPublicKey(encoded_network=rinkeby_network.definition),
EthereumSignMessage(message=b'', encoded_network=eth_network.definition), EthereumSignMessage(message=b'', encoded_network=rinkeby_network.definition),
EthereumSignTypedData(primary_type="", encoded_network=eth_network.definition), EthereumSignTypedData(primary_type="", encoded_network=rinkeby_network.definition),
EthereumVerifyMessage(signature=b'', message=b'', address="", encoded_network=eth_network.definition), EthereumVerifyMessage(signature=b'', message=b'', address="", encoded_network=rinkeby_network.definition),
] ]
for msg in messages: for msg in messages:
self.get_and_compare_ethereum_definitions(msg, eth_network.info, None) self.get_and_compare_ethereum_definitions(msg, rinkeby_network.info, None)
# neither network nor token should be loaded # neither network nor token should be loaded
messages = [ messages = [

View File

@ -2,10 +2,14 @@ from common import *
if not utils.BITCOIN_ONLY: if not utils.BITCOIN_ONLY:
from apps.ethereum import tokens from apps.ethereum import tokens
from ethereum_common import builtin_token_by_chain_address
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestEthereumTokens(unittest.TestCase): class TestEthereumTokens(unittest.TestCase):
def setUp(self):
# use mockup function for built-in tokens
tokens.token_by_chain_address = builtin_token_by_chain_address
def test_token_by_chain_address(self): def test_token_by_chain_address(self):