1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-16 11:28:14 +00:00

Merge pull request #272 from trezor/tsusanka/ripple

Add Ripple support
This commit is contained in:
Jan Pochyla 2018-07-30 17:00:01 +02:00 committed by GitHub
commit ce7ed00eb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 493 additions and 9 deletions

View File

@ -32,4 +32,4 @@ known_standard_library = micropython,ubinascii,ustruct,uctypes,utime,utimeq,trez
[tool:pytest]
addopts = --pyargs trezorlib.tests.device_tests
xfail_strict = true
run_xfail =
run_xfail = ripple

39
src/apps/ripple/README.md Normal file
View File

@ -0,0 +1,39 @@
# Ripple
MAINTAINER = Tomas Susanka <tomas.susanka@satoshilabs.com>
AUTHOR = Tomas Susanka <tomas.susanka@satoshilabs.com>
REVIEWER = Jan Pochyla <jan.pochyla@satoshilabs.com>
-----
## Documentation
Ripple's documentation can be found [here](https://developers.ripple.com/) and on the deprecated [wiki](https://wiki.ripple.com).
## Transactions
Ripple has different transaction types, see the [documentation](https://developers.ripple.com/transaction-formats.html) for the structure and the list of all transaction types. The concept is somewhat similar to Stellar. However, Stellar's transaction is composed of operations, whereas in Ripple each transaction is simply of some transaction type.
We do not support transaction types other than the [Payment](https://developers.ripple.com/payment.html) transaction, which represents the simple "A pays to B" scenario. Other transaction types might be added later on.
Non-XRP currencies are not supported. Float and negative amounts are not supported.
#### Transactions Explorer
[Bithomp](https://bithomp.com/) seems to work fine.
#### Submitting a transaction
You can use [ripple-lib](https://github.com/ripple/ripple-lib) and its [submit](https://github.com/ripple/ripple-lib/blob/develop/docs/index.md#submit) method to publish a transaction into the Ripple network. Python-trezor returns a serialized signed transaction, which is exactly what you provide as an argument into the submit function.
## Serialization format
Ripple uses its own [serialization format](https://wiki.ripple.com/Binary_Format). In a simple case, the first nibble of a first byte denotes the type and the second nibble the field. The actual data follow.
Our implementation in `serialize.py` is a simplification of the protocol tailored for the support of the Payment type exclusively.
## Tests
Unit tests are located in the `tests` directory, device tests are in the python-trezor repository.

View File

@ -0,0 +1,19 @@
from trezor.messages.MessageType import RippleGetAddress, RippleSignTx
from trezor.wire import protobuf_workflow, register
def dispatch_RippleGetAddress(*args, **kwargs):
from .get_address import get_address
return get_address(*args, **kwargs)
def dispatch_RippleSignTx(*args, **kwargs):
from .sign_tx import sign_tx
return sign_tx(*args, **kwargs)
def boot():
register(RippleGetAddress, protobuf_workflow, dispatch_RippleGetAddress)
register(RippleSignTx, protobuf_workflow, dispatch_RippleSignTx)

View File

@ -0,0 +1,33 @@
from trezor.crypto import base58
# Ripple uses different 58 character alphabet than traditional base58
_ripple_alphabet = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"
def encode(data: bytes) -> str:
"""
Convert bytes to base58 encoded string.
"""
return base58.encode(data, alphabet=_ripple_alphabet)
def decode(string: str) -> bytes:
"""
Convert base58 encoded string to bytes.
"""
return base58.decode(string, alphabet=_ripple_alphabet)
def encode_check(data: bytes, digestfunc=base58.sha256d_32) -> str:
"""
Convert bytes to base58 encoded string, append checksum.
"""
return encode(data + digestfunc(data))
def decode_check(string: str, digestfunc=base58.sha256d_32) -> bytes:
"""
Convert base58 encoded string to bytes and verify checksum.
"""
data = decode(string)
return base58.verify_checksum(data, digestfunc)

View File

@ -0,0 +1,22 @@
from trezor.messages.RippleAddress import RippleAddress
from trezor.messages.RippleGetAddress import RippleGetAddress
from . import helpers
from apps.common import seed
from apps.common.display_address import show_address, show_qr
async def get_address(ctx, msg: RippleGetAddress):
node = await seed.derive_node(ctx, msg.address_n)
pubkey = node.public_key()
address = helpers.address_from_public_key(pubkey)
if msg.show_display:
while True:
if await show_address(ctx, address):
break
if await show_qr(ctx, address.upper()):
break
return RippleAddress(address=address)

View File

@ -0,0 +1,47 @@
from micropython import const
from trezor.crypto.hashlib import ripemd160, sha256
from . import base58_ripple
# HASH_TX_ID = const(0x54584E00) # 'TXN'
HASH_TX_SIGN = const(0x53545800) # 'STX'
# HASH_TX_SIGN_TESTNET = const(0x73747800) # 'stx'
# https://developers.ripple.com/basic-data-types.html#specifying-currency-amounts
DIVISIBILITY = const(6) # 1000000 drops equal 1 XRP
# https://developers.ripple.com/transaction-cost.html
MIN_FEE = const(10)
# max is not defined officially but we check to make sure
MAX_FEE = const(1000000) # equals 1 XRP
FLAG_FULLY_CANONICAL = 0x80000000
def address_from_public_key(pubkey: bytes) -> str:
"""Extracts public key from an address
Ripple address is in format:
<1-byte ripple flag> <20-bytes account id> <4-bytes dSHA-256 checksum>
- 1-byte flag is 0x00 which is 'r' (Ripple uses its own base58 alphabet)
- 20-bytes account id is a ripemd160(sha256(pubkey))
- checksum is first 4 bytes of double sha256(data)
see https://developers.ripple.com/accounts.html#address-encoding
"""
"""Returns the Ripple address created using base58"""
h = sha256(pubkey).digest()
h = ripemd160(h).digest()
address = bytearray()
address.append(0x00) # 'r'
address.extend(h)
return base58_ripple.encode_check(bytes(address))
def decode_address(address: str):
"""Returns so called Account ID"""
adr = base58_ripple.decode_check(address)
return adr[1:]

25
src/apps/ripple/layout.py Normal file
View File

@ -0,0 +1,25 @@
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.text import Text
from trezor.utils import format_amount
from . import helpers
from apps.common.confirm import require_confirm, require_hold_to_confirm
from apps.common.display_address import split_address
async def require_confirm_fee(ctx, fee):
text = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN)
text.normal("Transaction fee:")
text.bold(format_amount(fee, helpers.DIVISIBILITY) + " XRP")
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_tx(ctx, to, value):
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
text.bold(format_amount(value, helpers.DIVISIBILITY) + " XRP")
text.normal("to")
text.mono(*split_address(to))
return await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)

View File

@ -0,0 +1,121 @@
# Serializes into the Ripple Format
#
# Inspired by https://github.com/miracle2k/ripple-python and https://github.com/ripple/ripple-lib
# Docs at https://wiki.ripple.com/Binary_Format and https://developers.ripple.com/transaction-common-fields.html
#
# The first four bits specify the field type (int16, int32, account..)
# the other four the record type (amount, fee, destination..) and then
# the actual data follow. This currently only supports the Payment
# transaction type and the fields that are required for it.
#
from trezor.messages.RippleSignTx import RippleSignTx
from . import helpers
FIELD_TYPE_INT16 = 1
FIELD_TYPE_INT32 = 2
FIELD_TYPE_AMOUNT = 6
FIELD_TYPE_VL = 7
FIELD_TYPE_ACCOUNT = 8
FIELDS_MAP = {
"account": {"type": FIELD_TYPE_ACCOUNT, "key": 1},
"amount": {"type": FIELD_TYPE_AMOUNT, "key": 1},
"destination": {"type": FIELD_TYPE_ACCOUNT, "key": 3},
"fee": {"type": FIELD_TYPE_AMOUNT, "key": 8},
"sequence": {"type": FIELD_TYPE_INT32, "key": 4},
"type": {"type": FIELD_TYPE_INT16, "key": 2},
"signingPubKey": {"type": FIELD_TYPE_VL, "key": 3},
"flags": {"type": FIELD_TYPE_INT32, "key": 2},
"txnSignature": {"type": FIELD_TYPE_VL, "key": 4},
"lastLedgerSequence": {"type": FIELD_TYPE_INT32, "key": 27},
}
TRANSACTION_TYPES = {"Payment": 0}
def serialize(msg: RippleSignTx, source_address: str, pubkey=None, signature=None):
w = bytearray()
# must be sorted numerically first by type and then by name
write(w, FIELDS_MAP["type"], TRANSACTION_TYPES["Payment"])
write(w, FIELDS_MAP["flags"], msg.flags)
write(w, FIELDS_MAP["sequence"], msg.sequence)
write(w, FIELDS_MAP["lastLedgerSequence"], msg.last_ledger_sequence)
write(w, FIELDS_MAP["amount"], msg.payment.amount)
write(w, FIELDS_MAP["fee"], msg.fee)
write(w, FIELDS_MAP["signingPubKey"], pubkey)
write(w, FIELDS_MAP["txnSignature"], signature)
write(w, FIELDS_MAP["account"], source_address)
write(w, FIELDS_MAP["destination"], msg.payment.destination)
return w
def write(w: bytearray, field: dict, value):
if value is None:
return
write_type(w, field)
if field["type"] == FIELD_TYPE_INT16:
w.extend(value.to_bytes(2, "big"))
elif field["type"] == FIELD_TYPE_INT32:
w.extend(value.to_bytes(4, "big"))
elif field["type"] == FIELD_TYPE_AMOUNT:
w.extend(serialize_amount(value))
elif field["type"] == FIELD_TYPE_ACCOUNT:
write_bytes(w, helpers.decode_address(value))
elif field["type"] == FIELD_TYPE_VL:
write_bytes(w, value)
else:
raise ValueError("Unknown field type")
def write_type(w: bytearray, field: dict):
if field["key"] <= 0xf:
w.append((field["type"] << 4) | field["key"])
else:
# this concerns two-bytes fields such as lastLedgerSequence
w.append(field["type"] << 4)
w.append(field["key"])
def serialize_amount(value: int) -> bytearray:
if value < 0 or isinstance(value, float):
raise ValueError("Only positive integers are supported")
if value > 100000000000: # max allowed value
raise ValueError("Value is larger than 100000000000")
b = bytearray(value.to_bytes(8, "big"))
# Clear first bit to indicate XRP
b[0] &= 0x7f
# Set second bit to indicate positive number
b[0] |= 0x40
return b
def write_bytes(w: bytearray, value: bytes):
"""Serialize a variable length bytes."""
serialize_varint(w, len(value))
w.extend(value)
def serialize_varint(w, val):
"""https://ripple.com/wiki/Binary_Format#Variable_Length_Data_Encoding"""
def rshift(val, n):
# http://stackoverflow.com/a/5833119/15677
return (val % 0x100000000) >> n
assert val >= 0
b = bytearray()
if val < 192:
b.append(val)
elif val <= 12480:
val -= 193
b.extend([193 + rshift(val, 8), val & 0xff])
elif val <= 918744:
val -= 12481
b.extend([241 + rshift(val, 16), rshift(val, 8) & 0xff, val & 0xff])
else:
raise ValueError("Variable integer overflow.")
w.extend(b)

View File

@ -0,0 +1,77 @@
from trezor.crypto import der
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha512
from trezor.messages.RippleSignedTx import RippleSignedTx
from trezor.messages.RippleSignTx import RippleSignTx
from trezor.wire import ProcessError
from . import helpers, layout
from .serialize import serialize
from apps.common import seed
async def sign_tx(ctx, msg: RippleSignTx):
validate(msg)
node = await seed.derive_node(ctx, msg.address_n)
source_address = helpers.address_from_public_key(node.public_key())
set_canonical_flag(msg)
tx = serialize(msg, source_address, pubkey=node.public_key())
to_sign = get_network_prefix() + tx
check_fee(msg.fee)
await layout.require_confirm_fee(ctx, msg.fee)
await layout.require_confirm_tx(ctx, msg.payment.destination, msg.payment.amount)
signature = ecdsa_sign(node.private_key(), first_half_of_sha512(to_sign))
tx = serialize(msg, source_address, pubkey=node.public_key(), signature=signature)
return RippleSignedTx(signature, tx)
def check_fee(fee: int):
if fee < helpers.MIN_FEE or fee > helpers.MAX_FEE:
raise ProcessError("Fee must be in the range of 10 to 10,000 drops")
def get_network_prefix():
"""Network prefix is prepended before the transaction and public key is included"""
return helpers.HASH_TX_SIGN.to_bytes(4, "big")
def first_half_of_sha512(b):
"""First half of SHA512, which Ripple uses"""
hash = sha512(b)
return hash.digest()[:32]
def ecdsa_sign(private_key: bytes, digest: bytes) -> bytes:
"""Signs and encodes signature into DER format"""
signature = secp256k1.sign(private_key, digest)
sig_der = der.encode_seq((signature[1:33], signature[33:65]))
return sig_der
def set_canonical_flag(msg: RippleSignTx):
"""
Our ECDSA implementation already returns fully-canonical signatures,
so we're enforcing it in the transaction using the designated flag
- see https://wiki.ripple.com/Transaction_Malleability#Using_Fully-Canonical_Signatures
- see https://github.com/trezor/trezor-crypto/blob/3e8974ff8871263a70b7fbb9a27a1da5b0d810f7/ecdsa.c#L791
"""
if msg.flags is None:
msg.flags = 0
msg.flags |= helpers.FLAG_FULLY_CANONICAL
def validate(msg: RippleSignTx):
if None in (
msg.fee,
msg.sequence,
msg.payment,
msg.payment.amount,
msg.payment.destination,
):
raise ProcessError(
"Some of the required fields are missing (fee, sequence, payment.amount, payment.destination)"
)

View File

@ -16,9 +16,9 @@ import apps.ethereum
import apps.lisk
import apps.nem
import apps.stellar
import apps.ripple
import apps.cardano
if __debug__:
import apps.debug
else:
@ -32,6 +32,7 @@ apps.ethereum.boot()
apps.lisk.boot()
apps.nem.boot()
apps.stellar.boot()
apps.ripple.boot()
apps.cardano.boot()
if __debug__:
apps.debug.boot()

View File

@ -17,7 +17,7 @@
_alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def encode(data: bytes) -> str:
def encode(data: bytes, alphabet=_alphabet) -> str:
"""
Convert bytes to base58 encoded string.
"""
@ -33,22 +33,22 @@ def encode(data: bytes) -> str:
result = ""
while acc > 0:
acc, mod = divmod(acc, 58)
result += _alphabet[mod]
result += alphabet[mod]
return "".join((c for c in reversed(result + _alphabet[0] * (origlen - newlen))))
return "".join((c for c in reversed(result + alphabet[0] * (origlen - newlen))))
def decode(string: str) -> bytes:
def decode(string: str, alphabet=_alphabet) -> bytes:
"""
Convert base58 encoded string to bytes.
"""
origlen = len(string)
string = string.lstrip(_alphabet[0])
string = string.lstrip(alphabet[0])
newlen = len(string)
p, acc = 1, 0
for c in reversed(string):
acc += p * _alphabet.index(c)
acc += p * alphabet.index(c)
p *= 58
result = []
@ -83,8 +83,12 @@ def decode_check(string: str, digestfunc=sha256d_32) -> bytes:
Convert base58 encoded string to bytes and verify checksum.
"""
result = decode(string)
return verify_checksum(result, digestfunc)
def verify_checksum(data: bytes, digestfunc) -> bytes:
digestlen = len(digestfunc(b""))
result, check = result[:-digestlen], result[-digestlen:]
result, check = data[:-digestlen], data[-digestlen:]
if check != digestfunc(result):
raise ValueError("Invalid checksum")

View File

@ -0,0 +1,19 @@
from common import *
from apps.ripple.helpers import address_from_public_key
class TestStellarPubkeyToAddress(unittest.TestCase):
def test_pubkey_to_address(self):
addr = address_from_public_key(unhexlify('ed9434799226374926eda3b54b1b461b4abf7237962eae18528fea67595397fa32'))
self.assertEqual(addr, 'rDTXLQ7ZKZVKz33zJbHjgVShjsBnqMBhmN')
addr = address_from_public_key(unhexlify('03e2b079e9b09ae8916da8f5ee40cbda9578dbe7c820553fe4d5f872eec7b1fdd4'))
self.assertEqual(addr, 'rhq549rEtUrJowuxQC2WsHNGLjAjBQdAe8')
addr = address_from_public_key(unhexlify('0282ee731039929e97db6aec242002e9aa62cd62b989136df231f4bb9b8b7c7eb2'))
self.assertEqual(addr, 'rKzE5DTyF9G6z7k7j27T2xEas2eMo85kmw')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,77 @@
from common import *
from apps.ripple.serialize import serialize
from apps.ripple.serialize import serialize_amount
from apps.ripple.sign_tx import get_network_prefix
from trezor.messages.RippleSignTx import RippleSignTx
from trezor.messages.RipplePayment import RipplePayment
class TestRippleSerializer(unittest.TestCase):
def test_amount(self):
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L2494
assert serialize_amount(0) == unhexlify('4000000000000000')
assert serialize_amount(1) == unhexlify('4000000000000001')
assert serialize_amount(93493429243) == unhexlify('40000015c4a483fb')
with self.assertRaises(ValueError):
serialize_amount(1000000000000000000) # too large
with self.assertRaises(ValueError):
serialize_amount(-1) # negative not supported
with self.assertRaises(ValueError):
serialize_amount(1.1) # float numbers not supported
def test_transactions(self):
# from https://github.com/miracle2k/ripple-python
source_address = 'r3P9vH81KBayazSTrQj6S25jW6kDb779Gi'
payment = RipplePayment(200000000, 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV')
common = RippleSignTx(None, 10, None, 1, None, payment)
assert serialize(common, source_address) == unhexlify('120000240000000161400000000bebc20068400000000000000a811450f97a072f1c4357f1ad84566a609479d927c9428314550fc62003e785dc231a1058a05e56e3f09cf4e6')
source_address = 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV'
payment = RipplePayment(1, 'r3P9vH81KBayazSTrQj6S25jW6kDb779Gi')
common = RippleSignTx(None, 99, None, 99, None, payment)
assert serialize(common, source_address) == unhexlify('12000024000000636140000000000000016840000000000000638114550fc62003e785dc231a1058a05e56e3f09cf4e6831450f97a072f1c4357f1ad84566a609479d927c942')
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L1579
source_address = 'r9TeThyi5xiuUUrFjtPKZiHcDxs7K9H6Rb'
payment = RipplePayment(25000000, 'r4BPgS7DHebQiU31xWELvZawwSG2fSPJ7C')
common = RippleSignTx(None, 10, 0, 2, None, payment)
assert serialize(common, source_address) == unhexlify('120000220000000024000000026140000000017d784068400000000000000a81145ccb151f6e9d603f394ae778acf10d3bece874f68314e851bbbe79e328e43d68f43445368133df5fba5a')
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L1651
source_address = 'rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e'
payment = RipplePayment(200000, 'rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ')
common = RippleSignTx(None, 15, 0, 144, None, payment)
# 201b005ee9ba removed from the test vector because last ledger sequence is not supported
assert serialize(common, source_address) == unhexlify('12000022000000002400000090614000000000030d4068400000000000000f8114aa1bd19d9e87be8069fdbf6843653c43837c03c6831467fe6ec28e0464dd24fb2d62a492aac697cfad02')
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L1732
source_address = 'r4BPgS7DHebQiU31xWELvZawwSG2fSPJ7C'
payment = RipplePayment(25000000, 'rBqSFEFg2B6GBMobtxnU1eLA1zbNC9NDGM')
common = RippleSignTx(None, 12, 0, 1, None, payment)
# 2ef72d50ca removed from the test vector because destination tag is not supported
assert serialize(common, source_address) == unhexlify('120000220000000024000000016140000000017d784068400000000000000c8114e851bbbe79e328e43d68f43445368133df5fba5a831476dac5e814cd4aa74142c3ab45e69a900e637aa2')
def test_transactions_for_signing(self):
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/signing-data-encoding-test.js
source_address = 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ'
payment = RipplePayment(1000, 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')
common = RippleSignTx(None, 10, 2147483648, 1, None, payment)
tx = serialize(common, source_address, pubkey=unhexlify('ed5f5ac8b98974a3ca843326d9b88cebd0560177b973ee0b149f782cfaa06dc66a'))
tx = get_network_prefix() + tx
assert tx[0:4] == unhexlify('53545800') # signing prefix
assert tx[4:7] == unhexlify('120000') # transaction type
assert tx[7:12] == unhexlify('2280000000') # flags
assert tx[12:17] == unhexlify('2400000001') # sequence
assert tx[17:26] == unhexlify('6140000000000003e8') # amount
assert tx[26:35] == unhexlify('68400000000000000a') # fee
assert tx[35:70] == unhexlify('7321ed5f5ac8b98974a3ca843326d9b88cebd0560177b973ee0b149f782cfaa06dc66a') # singing pub key
assert tx[70:92] == unhexlify('81145b812c9d57731e27a2da8b1830195f88ef32a3b6') # account
assert tx[92:114] == unhexlify('8314b5f762798a53d543a014caf8b297cff8f2f937e8') # destination
assert len(tx[114:]) == 0 # that's it
if __name__ == '__main__':
unittest.main()