Skip to content
This repository has been archived by the owner on Jun 5, 2019. It is now read-only.

Add cardano support to trezorctl and some tests #300

Merged
merged 8 commits into from
Sep 5, 2018

Conversation

ddeath
Copy link
Contributor

@ddeath ddeath commented Aug 18, 2018

Added device tests for get_address and sign_transaction calls.

To the trezorctl was also added get_public_key, sign and verify message

@tsusanka
Copy link
Contributor

@matejcik I can leave this for you to review, right?



@session
def cardano_sign_transaction(client, transaction):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename to sign_tx ("tx" instead of "transaction", no "cardano" prefix)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also the signature is not great, see comments in trezorctl and on create_sign_transaction_message.

In general, these functions should consume either simple values, or filled out protobuf messages. In this case, best I could come up with is:

def sign_tx(client, inputs: List[CardanoTxInputType], outputs: List[CardanoTxOutputType], transactions: List[bytes])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

trezorctl Outdated
@click.pass_obj
def cardano_sign_transaction(connect, file):
client = connect()
signed_transaction = cardano.cardano_sign_transaction(client, transaction=json.load(file))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should not put raw dict into the API call.
Instead, something like:

transaction = cardano.create_sign_tx(json.load(file))
signed_tx = cardano.sign_tx(client, transaction)

Also, how can the user create the JSON?
I expect there is a separate tool / script that generates it, but can it include the appropriate BIP32 paths?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with you metioned suggestion bellow


@session
def cardano_sign_transaction(client, transaction):
msg = create_sign_transaction_message(transaction)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be part of this function, see comment in trezorctl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

ack_message = create_tx_ack_message(transaction.get('transactions'), tx_index)
response = client.call(ack_message)

return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to return a dict, as opposed to a tuple (hash, body) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is so user will see what is hash and what is body without looking at code. if it is only tuple you will have hard times to identify body vs hash as both looks same

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not user-facing function though. please return a tuple and do the conversion in trezorctl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed



def create_sign_transaction_message(transaction) -> messages.CardanoSignTransaction:
if not all(transaction.get(k) for k in ('inputs', 'outputs', 'transactions')):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please extract the lists of required fields to the top level:

REQUIRED_FIELDS_TRANSACTION = ("inputs", "outputs"...
REQUIRED_FIELDS_INPUT = (...
REQUIRED_FIELDS_OUTPUT = (...

Don't repeat the fields in the ValueError either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed except output fields as there is only amount required and one other but it does not matter which one - well there must be at least one address_n (at least in one of the outputs)


outputs = []
for output in transaction.get('outputs'):
if not output.get('amount') or not(output.get('address') or output.get('path')):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the same all(...) check as above

alternately, a better approach could be ensure_required_fields(dict, REQUIRED_FIELDS) helper that raises the ValueError. In that case, you could even put the list of required fields into the error messages

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how could I use all whene there are not || not ( || ) which is same as not || (not && not)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, my bad, didn't notice the nesting

trezorlib/cardano.py Outdated Show resolved Hide resolved
while response.tx_index is not None:
tx_index = response.tx_index

ack_message = create_tx_ack_message(transaction.get('transactions'), tx_index)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the create_tx_ack_message is sort of pointless, esp. if you get transactions directly as an argument. please inline it here.

(also maybe verify that the requested index makes sense? but AFAICT the index is pointless anyway, so maybe it doesn't matter)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed without index verification

import pytest

from .common import TrezorTest
from .conftest import TREZOR_VERSION
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't seem to be using TREZOR_VERSION

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

class TestMsgCardanoSignTransaction(TrezorTest):

def test_cardano_sign_transaction(self):
self.setup_mnemonic_nopin_nopassphrase()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ISTM this test could be vastly simplified if you used set_expected_responses

see e.g. https://github.com/trezor/python-trezor/blob/master/trezorlib/tests/device_tests/test_msg_signtx.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to used that but I was unsure how to use it when I need to do swipes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the swipes thing again! Yes, it makes sense in that case. We need a better infrastructure for simulating swipes.

@matejcik
Copy link
Contributor

I have reviewed the changes. Please see comments and make modifications where appropriate.

In general, my main question is, how do you expect this to be used, from the user side? Is there a tool/service that generates the appropriate JSON data? If not, then, I suppose we'll keep it like this for now. I'm working on some changes that would make it more useful; namely, converting a dict to protobuf directly.

In any case, unless there are good reasons against, please go with my suggestion for the sign_tx() signature. Then it depends on where the JSON comes from. If the format is arbitrary, I suggest parsing it in trezorctl:

inputs = [cardano.create_input(input) for input in transaction['inputs']]
outputs = [cardano.create_output(output) for outputs in transaction['outputs']]
transactions = transaction['transactions']

If the format comes from somewhere else, I'd keep the create_transaction function, but make it return a tuple inputs, outputs, transactions as opposed to a protobuf message (which doesn't include the "transactions" field)`

@matejcik
Copy link
Contributor

Also please add the remaining tests (sign/verify message, get public key i think?)

@ddeath
Copy link
Contributor Author

ddeath commented Aug 21, 2018

@matejcik I tried to fix all your comments and also added tests...

About that JSON tool - there is no such a tool. If somebody want to use it then they could look here:
https://github.com/vacuumlabs/cardanolite/blob/78b03bbcf80a9b4d04444d0f954c7c8184ea636d/app/frontend/wallet/cardano-wallet.js#L82

There is our own code for creating tx or they can take a look to connect v5.

The CI is failing because of some other test fails to fetch some data from internet - I guess that we did not break it

ack_message = messages.CardanoTxAck(transaction=transaction_data)
response = client.call(ack_message)

return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return response.tx_hash, response.tx_body

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just returned response because it is CardanoSignedTx message now. Is that ok?

trezorctl Outdated Show resolved Hide resolved
@matejcik
Copy link
Contributor

Tests look reasonable. Please fix my latest comments, I'll look over the whole thing again tomorrow.

Travis failure is on our side; if you rebase on top of current master, it should go away.

@tsusanka
Copy link
Contributor

tsusanka commented Aug 21, 2018

Please also modify the signing test where now CardanoSignedTx message is returned (trezor/trezor-core@e657397) (if needed).

@ddeath
Copy link
Contributor Author

ddeath commented Aug 22, 2018

I fixed comments and also did the rebase.

@tsusanka
Copy link
Contributor

@ddeath we would like to unify what derivation paths are sent to which coins. We came up with this doc. We would like Cardano wallets to send 44'/1815'/a' for get public key and 44'/1815'/a'/0/i for get_address / sign.

  1. Is that okay on your side?
  2. Do you think you could modify the tests accordingly? I know it's a pain to regenerate the data, but the tests serve as a point of reference sometimes and it would be unfortunate to have it incorrect.

@ddeath
Copy link
Contributor Author

ddeath commented Aug 31, 2018

@tsusanka

  1. I would suggest to change get_address and sign to 44'/1815'/a'/{0,1}/i as I have almost ready PR for adding support for cardano derivation scheme v2 which uses 0 or 1 in that place. In the v1 it must be 0 but in v2 it can be 0 or 1
  2. I will regenerate the tests

@matejcik
Copy link
Contributor

I finally got to review the current state. It looks mostly OK.

Two things:

  1. please add at least one invalid sig to verify_message test :) this way it's not testing much.

  2. do the tests pass in the current trezor-core master? if not, they should all be marked xfail

@tsusanka
Copy link
Contributor

tsusanka commented Sep 2, 2018

I would suggest to change get_address and sign to 44'/1815'/a'/{0,1}/i as I have almost ready PR for adding support for cardano derivation scheme v2 which uses 0 or 1 in that place. In the v1 it must be 0 but in v2 it can be 0 or 1

Ok, agreed.

@ddeath
Copy link
Contributor Author

ddeath commented Sep 4, 2018

@tsusanka I changed test so it reflect the derivation paths. Please check if that is what you mean. Also I overlooked the get_address and sign_transaction derivation paths and would suggest to change it to 44'/1815'/a'/{0,1}/{i, i'} as the official cardano wallet Daedalus is working with addresses derived with i' it is also possible to use i but for example Binance has problems to accept these kind of addresses.
Also in derivation scheme v2 is used i by default in cardano new wallet Icarus.

@matejcik added invalid signatures tests. The test should be passing on master, but they are not because there was introduced bugs couple days ago in this commit where the name of messages are incorrectly swaped and also in this commit where the .ui was not renamed to .layout

So not sure if I should mark it as xfail. I will create a PR for adding derivation scheme v2 in couple of hours where I fixed it already. But not sure about your workflow so please just point me out what is the way to do it in this situation.

Thanks

@matejcik
Copy link
Contributor

matejcik commented Sep 4, 2018

Btw for further reference it would be great if you didn't squash the commit every time, so we could see changes from last time and not have to read through the whole thing.

If your code is in core, and the tests are supposed to pass, then no xfail is needed. xfail is for cases when the tests are submitted ahead of the device code.


@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
class TestMsgCardanoGetPublicKey(TrezorTest):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename to match test contents


@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
class TestMsgCardanoGetPublicKey(TrezorTest):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename test and method names, and consider using pytest.mark.parametrize

@pytest.mark.skip_t1 # T1 support is not planned
class TestMsgCardanoGetPublicKey(TrezorTest):

def test_cardano_get_public_key(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this

while you're at it, consider using @pytest.mark.parametrize to simplify test code here, see e.g. test_cancel


def test_cardano_get_public_key(self):
self.client.load_device_by_mnemonic(
mnemonic='plastic that delay conduct police ticket swim gospel intact harsh obtain entire',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason you're using this mnemonic? If not, please regenerate the sigs with the default all all all...

(a good reason would be, e.g., if these were officially documented test vectors)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was choosing this mnemonic because the same is in trezor-core

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case let's change it to the default all all all.... It should be changed in trezor-core too, but we'll probably do that later along with other currencies that use custom mnemonics for tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change it as I will be removing / changing all the tests because of change of derivation scheme in the next PR.

signature = sign_message(self.client, parse_path(derivation_path), message)
assert expected_signature == hexlify(signature.signature).decode('utf8')

incorrect_messages = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW you don't need to test invalid sigs here, because you're already testing against an expected value; it's very unlikely that a sign_message would produce the particular signature that you're testing it's not producing.

(as opposed to the verify_message method, where you need a test that it can fail, that it doesn't return True for every input)

unless of course these are vectors for some known failure mode

REQUIRED_FIELDS_INPUT = ('path', 'prev_hash', 'prev_index', 'type')


@expect(messages.CardanoAddress)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably extract the @field("address") directly, right?

assert address == '2w1sdSJu3GVhMmEYeGYEPWahV1V17pFw59GfgqjSRqa6x1rKFxbyCZrQWLe78xdSx3zyed6DrrN5yMgoY7ST2vJeaMzUDB7W3WG'
address = get_address(self.client, parse_path("m/44'/1815'/0'/0/2")).address
assert address == '2w1sdSJu3GVeHCDfy3mjq8RkzkN3Vh7Di3cB8NRzkwkLQ2FAjxX1kvkNdP9hNBzyBVEJdeWwyb5GfFYXgKe7rPgvWj2QD8FE4W3'
address = get_address(self.client, parse_path(path)).address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ISTM this will fail now that get_address returns the address directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it failed. Fixed. Also replaced custom mnemonic for all all...

@matejcik
Copy link
Contributor

matejcik commented Sep 4, 2018

In the meantime, I merged a big change into master. This should affect you very little, although you will probably get merge conflicts in trezorctl.

IMHO the safest way to resolve them is simply copy-paste your code from pre-merge trezorctl to post-merge trezorctl.

Another change that affects you is that @expect and @field are now the same decorator, so please modify that one use of @field to @expect(..., field='...')

afterwards, run make style (note you need python 3.6 for that)

@ddeath
Copy link
Contributor Author

ddeath commented Sep 5, 2018

@tsusanka please ignore last conversation about i vs i'. It should be ok as it is, because as stated here trezor/trezor-crypto#179 (comment) the derivation scheme v1 is going to be dropped

@ddeath
Copy link
Contributor Author

ddeath commented Sep 5, 2018

@matejcik should be fixed now

@tsusanka
Copy link
Contributor

tsusanka commented Sep 5, 2018

@tsusanka please ignore last conversation about i vs i'. It should be ok as it is, because as stated here trezor/trezor-crypto#179 (comment) the derivation scheme v1 is going to be dropped

Ok so 44'/1815'/a'/{0,1}/i it is.

@matejcik
Copy link
Contributor

matejcik commented Sep 5, 2018

looks good to merge from my side.

@tsusanka @ddeath any planned changes regarding the paths? Or should I merge it now and path changes will be resolved in a new PR?

@ddeath ddeath mentioned this pull request Sep 5, 2018
@tsusanka
Copy link
Contributor

tsusanka commented Sep 5, 2018

IMHO it is correct now and the tests seem to use correct paths.

@ddeath
Copy link
Contributor Author

ddeath commented Sep 5, 2018

I am ok with the current paths

Update submodule to trezor official one
@ddeath
Copy link
Contributor Author

ddeath commented Sep 5, 2018

@matejcik added changes for cadrano derivation path v2 and marked all test as xfail

@matejcik
Copy link
Contributor

matejcik commented Sep 5, 2018

Neat, merging. Now let's see what happens with core :)
Thanks!

@matejcik matejcik merged commit fb22b89 into trezor:master Sep 5, 2018
@matejcik matejcik added this to the v0.11 milestone Nov 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants