#28 - binance implementation (#189)

- placeOrder, cancelOrder, transfer messages
- cli support
- unit and device tests
pull/378/head
Ciny 5 years ago committed by matejcik
parent e4a69dcc53
commit 90b91a7fb5

@ -71,7 +71,6 @@ message BinanceTxRequest {
/**
* Request: Ask the device to include a Binance transfer msg in the tx.
* @next BinanceTxRequest
* @next BinanceSignedTx
* @next Failure
*/
@ -92,7 +91,6 @@ message BinanceTransferMsg {
/**
* Request: Ask the device to include a Binance order msg in the tx.
* @next BinanceTxRequest
* @next BinanceSignedTx
* @next Failure
*/
@ -129,7 +127,6 @@ message BinanceOrderMsg {
/**
* Request: Ask the device to include a Binance cancel msg in the tx.
* @next BinanceTxRequest
* @next BinanceSignedTx
* @next Failure
*/
@ -146,6 +143,5 @@ message BinanceCancelMsg {
message BinanceSignedTx {
optional bytes signature = 1;
optional bytes public_key = 2;
optional string json = 3;
}

@ -21,6 +21,7 @@ algorithm, extended to work on other curves.
| Ethereum | secp256k1 | `44'/c'/0'/0/a` | yes | [2](#Ethereum) |
| Ripple | secp256k1 | `44'/144'/a'/0/0` | | [3](#Ripple) |
| EOS | secp256k1 | `44'/194'/a'/0/0` | | [3](#Ripple) |
| Binance | secp256k1 | `44'/714'/a'/0/0` | | [3](#Ripple) |
| Tron | secp256k1 | TODO | | TODO |
| Ontology | nist256p1 | TODO | | TODO |
| Cardano | ed25519 | `44'/1815'/a'/y/i` | yes | [4](#Cardano) |

@ -0,0 +1,25 @@
# Binance
MAINTAINER = Stanislav Marcinko <stanislav.marcinko@satoshilabs.com>
AUTHOR = Stanislav Marcinko <stanislav.marcinko@satoshilabs.com>
REVIEWER = Jan Matějek <jan.matejek@satoshilabs.com>
-----
Implementation is based on [binance chain documentation](https://binance-chain.github.io/blockchain.html) and tested against fixtures from [binance javascript sdk](https://github.com/binance-chain/javascript-sdk/). Only a subset of messages needed for sdk integration is implemented.
## Transactions
Binance transaction consists of a transaction message wrapped in a standard transaction structure (see documentation). One message per transaction.
Implemented subset of messages:
Place Order
Cancel Order
Transfer
## Testing
Mnemonic for recovering wallet used in binance sdk tests:
offer caution gift cross surge pretty orange during eye soldier popular holiday mention east eight office fashion ill parrot vault rent devote earth cousin

@ -0,0 +1,13 @@
from trezor import wire
from trezor.messages import MessageType
from apps.common import HARDENED
CURVE = "secp256k1"
def boot() -> None:
ns = [[CURVE, HARDENED | 44, HARDENED | 714]]
wire.add(MessageType.BinanceGetAddress, __name__, "get_address", ns)
wire.add(MessageType.BinanceGetPublicKey, __name__, "get_public_key", ns)
wire.add(MessageType.BinanceSignTx, __name__, "sign_tx", ns)

@ -0,0 +1,27 @@
from trezor.messages.BinanceAddress import BinanceAddress
from trezor.messages.BinanceGetAddress import BinanceGetAddress
from apps.binance import CURVE, helpers
from apps.common import paths
from apps.common.layout import address_n_to_str, show_address, show_qr
async def get_address(ctx, msg: BinanceGetAddress, keychain):
HRP = "bnb"
await paths.validate_path(
ctx, helpers.validate_full_path, keychain, msg.address_n, CURVE
)
node = keychain.derive(msg.address_n)
pubkey = node.public_key()
address = helpers.address_from_public_key(pubkey, HRP)
if msg.show_display:
desc = address_n_to_str(msg.address_n)
while True:
if await show_address(ctx, address, desc=desc):
break
if await show_qr(ctx, address, desc=desc):
break
return BinanceAddress(address=address)

@ -0,0 +1,18 @@
from trezor.messages.BinanceGetPublicKey import BinanceGetPublicKey
from trezor.messages.BinancePublicKey import BinancePublicKey
from apps.binance import CURVE, helpers
from apps.common import layout, paths
async def get_public_key(ctx, msg: BinanceGetPublicKey, keychain):
await paths.validate_path(
ctx, helpers.validate_full_path, keychain, msg.address_n, CURVE
)
node = keychain.derive(msg.address_n)
pubkey = node.public_key()
if msg.show_display:
await layout.show_pubkey(ctx, pubkey)
return BinancePublicKey(pubkey)

@ -0,0 +1,115 @@
from micropython import const
from trezor.crypto import bech32
from trezor.crypto.scripts import sha256_ripemd160_digest
from trezor.messages.BinanceCancelMsg import BinanceCancelMsg
from trezor.messages.BinanceInputOutput import BinanceInputOutput
from trezor.messages.BinanceOrderMsg import BinanceOrderMsg
from trezor.messages.BinanceSignTx import BinanceSignTx
from trezor.messages.BinanceTransferMsg import BinanceTransferMsg
from apps.common import HARDENED
ENVELOPE_BLUEPRINT = '{{"account_number":"{account_number}","chain_id":"{chain_id}","data":null,"memo":"{memo}","msgs":[{msgs}],"sequence":"{sequence}","source":"{source}"}}'
MSG_TRANSFER_BLUEPRINT = '{{"inputs":[{inputs}],"outputs":[{outputs}]}}'
MSG_NEWORDER_BLUEPRINT = '{{"id":"{id}","ordertype":{ordertype},"price":{price},"quantity":{quantity},"sender":"{sender}","side":{side},"symbol":"{symbol}","timeinforce":{timeinforce}}}'
MSG_CANCEL_BLUEPRINT = '{{"refid":"{refid}","sender":"{sender}","symbol":"{symbol}"}}'
INPUT_OUTPUT_BLUEPRINT = '{{"address":"{address}","coins":[{coins}]}}'
COIN_BLUEPRINT = '{{"amount":"{amount}","denom":"{denom}"}}'
# 1*10^18 Jagers equal 1 BNB https://www.binance.vision/glossary/jager
DIVISIBILITY = const(18)
def produce_json_for_signing(envelope: BinanceSignTx, msg) -> str:
if isinstance(msg, BinanceTransferMsg):
json_msg = produce_transfer_json(msg)
elif isinstance(msg, BinanceOrderMsg):
json_msg = produce_neworder_json(msg)
elif isinstance(msg, BinanceCancelMsg):
json_msg = produce_cancel_json(msg)
else:
raise ValueError("input message unrecognized, is of type " + type(msg).__name__)
if envelope.source is None or envelope.source < 0:
raise ValueError("source missing or invalid")
source = envelope.source
return ENVELOPE_BLUEPRINT.format(
account_number=envelope.account_number,
chain_id=envelope.chain_id,
memo=envelope.memo,
msgs=json_msg,
sequence=envelope.sequence,
source=source,
)
def produce_transfer_json(msg: BinanceTransferMsg) -> str:
def make_input_output(input_output: BinanceInputOutput):
coins = ",".join(
COIN_BLUEPRINT.format(amount=c.amount, denom=c.denom)
for c in input_output.coins
)
return INPUT_OUTPUT_BLUEPRINT.format(address=input_output.address, coins=coins)
inputs = ",".join(make_input_output(i) for i in msg.inputs)
outputs = ",".join(make_input_output(o) for o in msg.outputs)
return MSG_TRANSFER_BLUEPRINT.format(inputs=inputs, outputs=outputs)
def produce_neworder_json(msg: BinanceOrderMsg) -> str:
return MSG_NEWORDER_BLUEPRINT.format(
id=msg.id,
ordertype=msg.ordertype,
price=msg.price,
quantity=msg.quantity,
sender=msg.sender,
side=msg.side,
symbol=msg.symbol,
timeinforce=msg.timeinforce,
)
def produce_cancel_json(msg: BinanceCancelMsg) -> str:
return MSG_CANCEL_BLUEPRINT.format(
refid=msg.refid, sender=msg.sender, symbol=msg.symbol
)
def address_from_public_key(pubkey: bytes, hrp: str) -> str:
"""
Address = RIPEMD160(SHA256(compressed public key))
Address_Bech32 = HRP + '1' + bech32.encode(convert8BitsTo5Bits(RIPEMD160(SHA256(compressed public key))))
HRP - bnb for productions, tbnb for tests
"""
h = sha256_ripemd160_digest(pubkey)
convertedbits = bech32.convertbits(h, 8, 5, False)
return bech32.bech32_encode(hrp, convertedbits)
def validate_full_path(path: list) -> bool:
"""
Validates derivation path to equal 44'/714'/a'/0/0,
where `a` is an account index from 0 to 1 000 000.
Similar to Ethereum this should be 44'/714'/a', but for
compatibility with other HW vendors we use 44'/714'/a'/0/0.
"""
if len(path) != 5:
return False
if path[0] != 44 | HARDENED:
return False
if path[1] != 714 | HARDENED:
return False
if path[2] < HARDENED or path[2] > 1000000 | HARDENED:
return False
if path[3] != 0:
return False
if path[4] != 0:
return False
return True

@ -0,0 +1,71 @@
from trezor import ui
from trezor.messages import (
BinanceCancelMsg,
BinanceInputOutput,
BinanceOrderMsg,
BinanceOrderSide,
BinanceTransferMsg,
ButtonRequestType,
)
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import format_amount
from . import helpers
from apps.common.confirm import hold_to_confirm
from apps.common.layout import split_address
async def require_confirm_transfer(ctx, msg: BinanceTransferMsg):
def make_input_output_pages(msg: BinanceInputOutput, direction):
pages = []
for coin in msg.coins:
coin_page = Text("Confirm " + direction, ui.ICON_SEND, icon_color=ui.GREEN)
coin_page.bold(
format_amount(coin.amount, helpers.DIVISIBILITY) + " " + coin.denom
)
coin_page.normal("to")
coin_page.mono(*split_address(msg.address))
pages.append(coin_page)
return pages
pages = []
for txinput in msg.inputs:
pages.extend(make_input_output_pages(txinput, "input"))
for txoutput in msg.outputs:
pages.extend(make_input_output_pages(txoutput, "output"))
return await hold_to_confirm(ctx, Paginated(pages), ButtonRequestType.ConfirmOutput)
async def require_confirm_cancel(ctx, msg: BinanceCancelMsg):
text = Text("Confirm cancel", ui.ICON_SEND, icon_color=ui.GREEN)
text.normal("Reference id:")
text.bold(msg.refid)
return await hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_order(ctx, msg: BinanceOrderMsg):
page1 = Text("Confirm order", ui.ICON_SEND, icon_color=ui.GREEN)
page1.normal("Sender address:")
page1.bold(msg.sender)
page2 = Text("Confirm order", ui.ICON_SEND, icon_color=ui.GREEN)
page2.normal("side:")
if msg.side == BinanceOrderSide.BUY:
page2.bold("buy")
elif msg.side == BinanceOrderSide.SELL:
page2.bold("sell")
page3 = Text("Confirm order", ui.ICON_SEND, icon_color=ui.GREEN)
page3.normal("Quantity:")
page3.bold(str(msg.quantity))
page3.normal("Price:")
page3.bold(str(msg.price))
return await hold_to_confirm(
ctx, Paginated([page1, page2, page3]), ButtonRequestType.SignTx
)

@ -0,0 +1,51 @@
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha256
from trezor.messages import MessageType
from trezor.messages.BinanceCancelMsg import BinanceCancelMsg
from trezor.messages.BinanceOrderMsg import BinanceOrderMsg
from trezor.messages.BinanceSignedTx import BinanceSignedTx
from trezor.messages.BinanceTransferMsg import BinanceTransferMsg
from trezor.messages.BinanceTxRequest import BinanceTxRequest
from apps.binance import CURVE, helpers, layout
from apps.common import paths
async def sign_tx(ctx, envelope, keychain):
# create transaction message -> sign it -> create signature/pubkey message -> serialize all
if envelope.msg_count > 1:
raise ValueError("Multiple messages not supported")
await paths.validate_path(
ctx, helpers.validate_full_path, keychain, envelope.address_n, CURVE
)
node = keychain.derive(envelope.address_n)
tx_req = BinanceTxRequest()
msg = await ctx.call_any(
tx_req,
MessageType.BinanceCancelMsg,
MessageType.BinanceOrderMsg,
MessageType.BinanceTransferMsg,
)
msg_json = helpers.produce_json_for_signing(envelope, msg)
if isinstance(msg, BinanceTransferMsg):
await layout.require_confirm_transfer(ctx, msg)
elif isinstance(msg, BinanceOrderMsg):
await layout.require_confirm_order(ctx, msg)
elif isinstance(msg, BinanceCancelMsg):
await layout.require_confirm_cancel(ctx, msg)
else:
raise ValueError("input message unrecognized, is of type " + type(msg).__name__)
signature_bytes = generate_content_signature(msg_json.encode(), node.private_key())
return BinanceSignedTx(signature=signature_bytes, public_key=node.public_key())
def generate_content_signature(json: bytes, private_key: bytes) -> bytes:
msghash = sha256(json).digest()
return secp256k1.sign(private_key, msghash)[1:65]

@ -41,6 +41,7 @@ def _boot_default():
import apps.cardano
import apps.tezos
import apps.eos
import apps.binance
if __debug__:
import apps.debug
@ -60,6 +61,7 @@ def _boot_default():
apps.cardano.boot()
apps.tezos.boot()
apps.eos.boot()
apps.binance.boot()
if __debug__:
apps.debug.boot()
else:

@ -0,0 +1,25 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceAddress(p.MessageType):
MESSAGE_WIRE_TYPE = 701
def __init__(
self,
address: str = None,
) -> None:
self.address = address
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address', p.UnicodeType, 0),
}

@ -0,0 +1,31 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceCancelMsg(p.MessageType):
MESSAGE_WIRE_TYPE = 708
def __init__(
self,
refid: str = None,
sender: str = None,
symbol: str = None,
) -> None:
self.refid = refid
self.sender = sender
self.symbol = symbol
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('refid', p.UnicodeType, 0),
2: ('sender', p.UnicodeType, 0),
3: ('symbol', p.UnicodeType, 0),
}

@ -0,0 +1,27 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceCoin(p.MessageType):
def __init__(
self,
amount: int = None,
denom: str = None,
) -> None:
self.amount = amount
self.denom = denom
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('amount', p.SVarintType, 0),
2: ('denom', p.UnicodeType, 0),
}

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceGetAddress(p.MessageType):
MESSAGE_WIRE_TYPE = 700
def __init__(
self,
address_n: List[int] = None,
show_display: bool = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.show_display = show_display
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('show_display', p.BoolType, 0),
}

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceGetPublicKey(p.MessageType):
MESSAGE_WIRE_TYPE = 702
def __init__(
self,
address_n: List[int] = None,
show_display: bool = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.show_display = show_display
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('show_display', p.BoolType, 0),
}

@ -0,0 +1,29 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
from .BinanceCoin import BinanceCoin
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceInputOutput(p.MessageType):
def __init__(
self,
address: str = None,
coins: List[BinanceCoin] = None,
) -> None:
self.address = address
self.coins = coins if coins is not None else []
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address', p.UnicodeType, 0),
2: ('coins', BinanceCoin, p.FLAG_REPEATED),
}

@ -0,0 +1,46 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceOrderMsg(p.MessageType):
MESSAGE_WIRE_TYPE = 707
def __init__(
self,
id: str = None,
ordertype: int = None,
price: int = None,
quantity: int = None,
sender: str = None,
side: int = None,
symbol: str = None,
timeinforce: int = None,
) -> None:
self.id = id
self.ordertype = ordertype
self.price = price
self.quantity = quantity
self.sender = sender
self.side = side
self.symbol = symbol
self.timeinforce = timeinforce
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('id', p.UnicodeType, 0),
2: ('ordertype', p.UVarintType, 0),
3: ('price', p.SVarintType, 0),
4: ('quantity', p.SVarintType, 0),
5: ('sender', p.UnicodeType, 0),
6: ('side', p.UVarintType, 0),
7: ('symbol', p.UnicodeType, 0),
8: ('timeinforce', p.UVarintType, 0),
}

@ -0,0 +1,5 @@
# Automatically generated by pb2py
# fmt: off
SIDE_UNKNOWN = 0
BUY = 1
SELL = 2

@ -0,0 +1,6 @@
# Automatically generated by pb2py
# fmt: off
OT_UNKNOWN = 0
MARKET = 1
LIMIT = 2
OT_RESERVED = 3

@ -0,0 +1,25 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinancePublicKey(p.MessageType):
MESSAGE_WIRE_TYPE = 703
def __init__(
self,
public_key: bytes = None,
) -> None:
self.public_key = public_key
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('public_key', p.BytesType, 0),
}

@ -0,0 +1,43 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceSignTx(p.MessageType):
MESSAGE_WIRE_TYPE = 704
def __init__(
self,
address_n: List[int] = None,
msg_count: int = None,
account_number: int = None,
chain_id: str = None,
memo: str = None,
sequence: int = None,
source: int = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.msg_count = msg_count
self.account_number = account_number
self.chain_id = chain_id
self.memo = memo
self.sequence = sequence
self.source = source
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('msg_count', p.UVarintType, 0),
3: ('account_number', p.SVarintType, 0),
4: ('chain_id', p.UnicodeType, 0),
5: ('memo', p.UnicodeType, 0),
6: ('sequence', p.SVarintType, 0),
7: ('source', p.SVarintType, 0),
}

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceSignedTx(p.MessageType):
MESSAGE_WIRE_TYPE = 709
def __init__(
self,
signature: bytes = None,
public_key: bytes = None,
) -> None:
self.signature = signature
self.public_key = public_key
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('signature', p.BytesType, 0),
2: ('public_key', p.BytesType, 0),
}

@ -0,0 +1,6 @@
# Automatically generated by pb2py
# fmt: off
TIF_UNKNOWN = 0
GTE = 1
TIF_RESERVED = 2
IOC = 3

@ -0,0 +1,30 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
from .BinanceInputOutput import BinanceInputOutput
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceTransferMsg(p.MessageType):
MESSAGE_WIRE_TYPE = 706
def __init__(
self,
inputs: List[BinanceInputOutput] = None,
outputs: List[BinanceInputOutput] = None,
) -> None:
self.inputs = inputs if inputs is not None else []
self.outputs = outputs if outputs is not None else []
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('inputs', BinanceInputOutput, p.FLAG_REPEATED),
2: ('outputs', BinanceInputOutput, p.FLAG_REPEATED),
}

@ -0,0 +1,13 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import Dict, List, Optional
except ImportError:
Dict, List, Optional = None, None, None # type: ignore
class BinanceTxRequest(p.MessageType):
MESSAGE_WIRE_TYPE = 705

@ -0,0 +1,49 @@
from common import *
from apps.common.paths import HARDENED
from apps.binance.helpers import address_from_public_key, validate_full_path
from trezor.crypto.curve import secp256k1
from ubinascii import unhexlify
class TestBinanceAddress(unittest.TestCase):
def test_privkey_to_address(self):
#source of test data - binance javascript SDK
privkey = "90335b9d2153ad1a9799a3ccc070bd64b4164e9642ee1dd48053c33f9a3a05e9"
expected_address = "tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd"
pubkey = secp256k1.publickey(unhexlify(privkey), True)
address = address_from_public_key(pubkey, "tbnb")
self.assertEqual(address, expected_address)
def test_paths(self):
# 44'/714'/a'/0/0 is correct
incorrect_paths = [
[44 | HARDENED],
[44 | HARDENED, 714 | HARDENED],
[44 | HARDENED, 714 | HARDENED, 0],
[44 | HARDENED, 714 | HARDENED, 0 | HARDENED, 0 | HARDENED],
[44 | HARDENED, 714 | HARDENED, 0 | HARDENED, 0 | HARDENED, 0 | HARDENED],
[44 | HARDENED, 714 | HARDENED, 0 | HARDENED, 1, 0],
[44 | HARDENED, 714 | HARDENED, 0 | HARDENED, 0, 5],
[44 | HARDENED, 714 | HARDENED, 9999 | HARDENED],
[44 | HARDENED, 714 | HARDENED, 9999000 | HARDENED, 0, 0],
[44 | HARDENED, 60 | HARDENED, 0 | HARDENED, 0, 0],
[1 | HARDENED, 1 | HARDENED, 1 | HARDENED],
]
correct_paths = [
[44 | HARDENED, 714 | HARDENED, 0 | HARDENED, 0, 0],
[44 | HARDENED, 714 | HARDENED, 3 | HARDENED, 0, 0],
[44 | HARDENED, 714 | HARDENED, 9 | HARDENED, 0, 0],
]
for path in incorrect_paths:
self.assertFalse(validate_full_path(path))
for path in correct_paths:
self.assertTrue(validate_full_path(path))
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,108 @@
from common import *
from apps.binance.helpers import produce_json_for_signing
from apps.binance.sign_tx import generate_content_signature, sign_tx
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha256
from trezor.messages.BinanceCancelMsg import BinanceCancelMsg
from trezor.messages.BinanceCoin import BinanceCoin
from trezor.messages.BinanceInputOutput import BinanceInputOutput
from trezor.messages.BinanceOrderMsg import BinanceOrderMsg
from trezor.messages.BinanceSignTx import BinanceSignTx
from trezor.messages.BinanceTransferMsg import BinanceTransferMsg
class TestBinanceSign(unittest.TestCase):
def test_order_signature(self):
# source of testing data
# https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/fixtures/placeOrder.json
json_hex_msg = "7b226163636f756e745f6e756d626572223a223334222c22636861696e5f6964223a2242696e616e63652d436861696e2d4e696c65222c2264617461223a6e756c6c2c226d656d6f223a22222c226d736773223a5b7b226964223a22424133364630464144373444384634313034353436334534373734463332384634414637373945352d3333222c226f7264657274797065223a322c227072696365223a3130303030303030302c227175616e74697479223a3130303030303030302c2273656e646572223a2274626e623168676d3070376b68666b38357a707a3576306a38776e656a33613930773730397a7a6c666664222c2273696465223a312c2273796d626f6c223a224144412e422d4236335f424e42222c2274696d65696e666f726365223a317d5d2c2273657175656e6365223a223332222c22736f75726365223a2231227d"
expected_signature = "851fc9542342321af63ecbba7d3ece545f2a42bad01ba32cff5535b18e54b6d3106e10b6a4525993d185a1443d9a125186960e028eabfdd8d76cf70a3a7e3100"
public_key = "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e"
private_key = "90335b9d2153ad1a9799a3ccc070bd64b4164e9642ee1dd48053c33f9a3a05e9"
# Testing data for object creation is decoded from json_hex_msg
envelope = BinanceSignTx(msg_count=1, account_number=34, chain_id="Binance-Chain-Nile", memo="", sequence=32, source=1)
msg = BinanceOrderMsg(id="BA36F0FAD74D8F41045463E4774F328F4AF779E5-33",
ordertype=2,
price=100000000,
quantity=100000000,
sender="tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd",
side=1,
symbol="ADA.B-B63_BNB",
timeinforce=1)
msg_json = produce_json_for_signing(envelope, msg)
#check if our json string produced for signing is the same as test vector
self.assertEqual(hexlify(msg_json).decode(), json_hex_msg)
#verify signature against public key
signature = generate_content_signature(msg_json.encode(), unhexlify(private_key))
self.assertTrue(verify_content_signature(unhexlify(public_key), signature, unhexlify(json_hex_msg)))
#check if the signed data is the same as test vector
self.assertEqual(signature, unhexlify(expected_signature))
def test_cancel_signature(self):
# source of testing data
# https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/fixtures/cancelOrder.json
json_hex_msg = "7b226163636f756e745f6e756d626572223a223334222c22636861696e5f6964223a2242696e616e63652d436861696e2d4e696c65222c2264617461223a6e756c6c2c226d656d6f223a22222c226d736773223a5b7b227265666964223a22424133364630464144373444384634313034353436334534373734463332384634414637373945352d3239222c2273656e646572223a2274626e623168676d3070376b68666b38357a707a3576306a38776e656a33613930773730397a7a6c666664222c2273796d626f6c223a2242434853562e422d3130465f424e42227d5d2c2273657175656e6365223a223333222c22736f75726365223a2231227d"
expected_signature = "d93fb0402b2b30e7ea08e123bb139ad68bf0a1577f38592eb22d11e127f09bbd3380f29b4bf15bdfa973454c5c8ed444f2e256e956fe98cfd21e886a946e21e5"
public_key = "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e"
private_key = "90335b9d2153ad1a9799a3ccc070bd64b4164e9642ee1dd48053c33f9a3a05e9"
# Testing data for object creation is decoded from json_hex_msg
envelope = BinanceSignTx(msg_count=1, account_number=34, chain_id="Binance-Chain-Nile", memo="", sequence=33, source=1)
msg = BinanceCancelMsg(refid="BA36F0FAD74D8F41045463E4774F328F4AF779E5-29",
sender="tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd",
symbol="BCHSV.B-10F_BNB")
msg_json = produce_json_for_signing(envelope, msg)
#check if our json string produced for signing is the same as test vector
self.assertEqual(hexlify(msg_json).decode(), json_hex_msg)
#verify signature against public key
signature = generate_content_signature(msg_json.encode(), unhexlify(private_key))
self.assertTrue(verify_content_signature(unhexlify(public_key), signature, unhexlify(json_hex_msg)))
#check if the signed data is the same as test vector
self.assertEqual(signature, unhexlify(expected_signature))
def test_transfer_signature(self):
# source of testing data
# https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/fixtures/transfer.json
json_hex_msg = "7b226163636f756e745f6e756d626572223a223334222c22636861696e5f6964223a2242696e616e63652d436861696e2d4e696c65222c2264617461223a6e756c6c2c226d656d6f223a2274657374222c226d736773223a5b7b22696e70757473223a5b7b2261646472657373223a2274626e623168676d3070376b68666b38357a707a3576306a38776e656a33613930773730397a7a6c666664222c22636f696e73223a5b7b22616d6f756e74223a2231303030303030303030222c2264656e6f6d223a22424e42227d5d7d5d2c226f757470757473223a5b7b2261646472657373223a2274626e6231737335376538736137786e77713033306b3263747237373575616339676a7a676c7168767079222c22636f696e73223a5b7b22616d6f756e74223a2231303030303030303030222c2264656e6f6d223a22424e42227d5d7d5d7d5d2c2273657175656e6365223a223331222c22736f75726365223a2231227d"
expected_signature = "97b4c2e41b0d0f61ddcf4020fff0ecb227d6df69b3dd7e657b34be0e32b956e22d0c6be5832d25353ae24af0bb223d4a5337320518c4e7708b84c8e05eb6356b"
public_key = "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e"
private_key = "90335b9d2153ad1a9799a3ccc070bd64b4164e9642ee1dd48053c33f9a3a05e9"
# Testing data for object creation is decoded from json_hex_msg
envelope = BinanceSignTx(msg_count=1, account_number=34, chain_id="Binance-Chain-Nile", memo="test", sequence=31, source=1)
coin = BinanceCoin(denom="BNB", amount=1000000000)
first_input = BinanceInputOutput(address="tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd", coins=[coin])
first_output = BinanceInputOutput(address="tbnb1ss57e8sa7xnwq030k2ctr775uac9gjzglqhvpy", coins=[coin])
msg = BinanceTransferMsg(inputs=[first_input], outputs=[first_output])
msg_json = produce_json_for_signing(envelope, msg)
#check if our json string produced for signing is the same as test vector
self.assertEqual(hexlify(msg_json).decode(), json_hex_msg)
#verify signature against public key
signature = generate_content_signature(msg_json.encode(), unhexlify(private_key))
self.assertTrue(verify_content_signature(unhexlify(public_key), signature, unhexlify(json_hex_msg)))
#check if the signed data is the same as test vector
self.assertEqual(signature, unhexlify(expected_signature))
def verify_content_signature(
public_key: bytes, signature: bytes, unsigned_data: bytes
) -> bool:
msghash = sha256(unsigned_data).digest()
return secp256k1.verify(public_key, signature, msghash)
if __name__ == '__main__':
unittest.main()

@ -31,6 +31,7 @@ import click
import requests
from trezorlib import (
binance,
btc,
cardano,
coins,
@ -1841,6 +1842,56 @@ def tezos_sign_tx(connect, address, file):
return tezos.sign_tx(client, address_n, msg)
#
# Binance functions
#
@cli.command(help="Get Binance address for specified path.")
@click.option(
"-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/714'/0'/0/0"
)
@click.option("-d", "--show-display", is_flag=True)
@click.pass_obj
def binance_get_address(connect, address, show_display):
client = connect()
address_n = tools.parse_path(address)
return binance.get_address(client, address_n, show_display)
@cli.command(help="Get Binance public key.")
@click.option(
"-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/714'/0'/0/0"
)
@click.option("-d", "--show-display", is_flag=True)
@click.pass_obj
def binance_get_public_key(connect, address, show_display):
client = connect()
address_n = tools.parse_path(address)
return binance.get_public_key(client, address_n, show_display).hex()
@cli.command(help="Sign Binance transaction")
@click.option(
"-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/714'/0'/0/0"
)
@click.option(
"-f",
"--file",
type=click.File("r"),
required=True,
help="Transaction in JSON format",
)
@click.pass_obj
def binance_sign_tx(connect, address, file):
client = connect()
address_n = tools.parse_path(address)
return binance.sign_tx(client, address_n, json.load(file))
#
# Main
#

@ -0,0 +1,52 @@
from . import messages
from .protobuf import dict_to_proto
from .tools import expect, session
@expect(messages.BinanceAddress, field="address")
def get_address(client, address_n, show_display=False):
return client.call(
messages.BinanceGetAddress(address_n=address_n, show_display=show_display)
)
@expect(messages.BinancePublicKey, field="public_key")
def get_public_key(client, address_n, show_display=False):
return client.call(
messages.BinanceGetPublicKey(address_n=address_n, show_display=show_display)
)
@session
def sign_tx(client, address_n, tx_json):
msg = tx_json["msgs"][0]
envelope = dict_to_proto(messages.BinanceSignTx, tx_json)
envelope.msg_count = 1
envelope.address_n = address_n
response = client.call(envelope)
if not isinstance(response, messages.BinanceTxRequest):
raise RuntimeError(
"Invalid response, expected BinanceTxRequest, received "
+ type(response).__name__
)
if "refid" in msg:
msg = dict_to_proto(messages.BinanceCancelMsg, msg)
elif "inputs" in msg:
msg = dict_to_proto(messages.BinanceTransferMsg, msg)
elif "ordertype" in msg:
msg = dict_to_proto(messages.BinanceOrderMsg, msg)
else:
raise ValueError("can not determine msg type")
response = client.call(msg)
if not isinstance(response, messages.BinanceSignedTx):
raise RuntimeError(
"Invalid response, expected BinanceSignedTx, received "
+ type(response).__name__
)
return response

@ -16,16 +16,13 @@ class BinanceSignedTx(p.MessageType):
self,
signature: bytes = None,
public_key: bytes = None,
json: str = None,
) -> None:
self.signature = signature
self.public_key = public_key
self.json = json
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('signature', p.BytesType, 0),
2: ('public_key', p.BytesType, 0),
3: ('json', p.UnicodeType, 0),
}

@ -1,3 +1,4 @@
binance
capricoin
cardano
decred

@ -0,0 +1,24 @@
import pytest
from trezorlib.binance import get_address
from trezorlib.tools import parse_path
from .conftest import setup_client
BINANCE_ADDRESS_TEST_VECTORS = [
("m/44'/714'/0'/0/0", "bnb1hgm0p7khfk85zpz5v0j8wnej3a90w709vhkdfu"),
("m/44'/714'/0'/0/1", "bnb1egswqkszzfc2uq78zjslc6u2uky4pw46x4rstd"),
]
@pytest.mark.binance
@pytest.mark.skip_t1 # T1 support is not planned
@setup_client(
mnemonic="offer caution gift cross surge pretty orange during eye soldier popular holiday mention east eight office fashion ill parrot vault rent devote earth cousin"
)
@pytest.mark.parametrize("path, expected_address", BINANCE_ADDRESS_TEST_VECTORS)
def test_binance_get_address(client, path, expected_address):
# data from https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/crypto.test.js#L50
address = get_address(client, parse_path(path))
assert address == expected_address

@ -0,0 +1,21 @@
import pytest
from trezorlib import binance
from trezorlib.tools import parse_path
from .conftest import setup_client
BINANCE_PATH = parse_path("m/44h/714h/0h/0/0")
@pytest.mark.binance
@pytest.mark.skip_t1 # T1 support is not planned
@setup_client(
mnemonic="offer caution gift cross surge pretty orange during eye soldier popular holiday mention east eight office fashion ill parrot vault rent devote earth cousin"
)
def test_binance_get_public_key(client):
sig = binance.get_public_key(client, BINANCE_PATH)
assert (
sig.hex()
== "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e"
)

@ -0,0 +1,100 @@
import pytest
from trezorlib import binance
from trezorlib.tools import parse_path
from .conftest import setup_client
BINANCE_TEST_VECTORS = [
( # CANCEL
{
"account_number": "34",
"chain_id": "Binance-Chain-Nile",
"data": "null",
"memo": "",
"msgs": [
{
"refid": "BA36F0FAD74D8F41045463E4774F328F4AF779E5-29",
"sender": "tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd",
"symbol": "BCHSV.B-10F_BNB",
}
],
"sequence": "33",
"source": "1",
},
{
"public_key": "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e",
"signature": "d93fb0402b2b30e7ea08e123bb139ad68bf0a1577f38592eb22d11e127f09bbd3380f29b4bf15bdfa973454c5c8ed444f2e256e956fe98cfd21e886a946e21e5",
},
),
( # ORDER
{
"account_number": "34",
"chain_id": "Binance-Chain-Nile",
"data": "null",
"memo": "",
"msgs": [
{
"id": "BA36F0FAD74D8F41045463E4774F328F4AF779E5-33",
"ordertype": 2,
"price": 100000000,
"quantity": 100000000,
"sender": "tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd",
"side": 1,
"symbol": "ADA.B-B63_BNB",
"timeinforce": 1,
}
],
"sequence": "32",
"source": "1",
},
{
"public_key": "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e",
"signature": "851fc9542342321af63ecbba7d3ece545f2a42bad01ba32cff5535b18e54b6d3106e10b6a4525993d185a1443d9a125186960e028eabfdd8d76cf70a3a7e3100",
},
),
( # TRANSFER
{
"account_number": "34",
"chain_id": "Binance-Chain-Nile",
"data": "null",
"memo": "test",
"msgs": [
{
"inputs": [
{
"address": "tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd",
"coins": [{"amount": 1000000000, "denom": "BNB"}],
}
],
"outputs": [
{
"address": "tbnb1ss57e8sa7xnwq030k2ctr775uac9gjzglqhvpy",
"coins": [{"amount": 1000000000, "denom": "BNB"}],
}
],
}
],
"sequence": "31",
"source": "1",
},
{
"public_key": "029729a52e4e3c2b4a4e52aa74033eedaf8ba1df5ab6d1f518fd69e67bbd309b0e",
"signature": "97b4c2e41b0d0f61ddcf4020fff0ecb227d6df69b3dd7e657b34be0e32b956e22d0c6be5832d25353ae24af0bb223d4a5337320518c4e7708b84c8e05eb6356b",
},
),
]
@pytest.mark.binance
@pytest.mark.skip_t1 # T1 support is not planned
@setup_client(
mnemonic="offer caution gift cross surge pretty orange during eye soldier popular holiday mention east eight office fashion ill parrot vault rent devote earth cousin"
)
@pytest.mark.parametrize("message, expected_response", BINANCE_TEST_VECTORS)
def test_binance_sign_message(client, message, expected_response):
response = binance.sign_tx(client, parse_path("m/44'/714'/0'/0/0"), message)
assert response.public_key.hex() == expected_response["public_key"]
assert response.signature.hex() == expected_response["signature"]

@ -5,6 +5,7 @@ PROTOB=common/protob
CORE_PROTOBUF_SOURCES="\
$PROTOB/messages.proto \
$PROTOB/messages-binance.proto \
$PROTOB/messages-bitcoin.proto \
$PROTOB/messages-cardano.proto \
$PROTOB/messages-common.proto \

Loading…
Cancel
Save