mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-23 23:08:14 +00:00
ripple: simple tx serializer; signing
This supports only fraction of the whole binary format Ripple uses. This is focused on the Payment transaction type, which is currently solely supported.
This commit is contained in:
parent
b5c2ae49dd
commit
08945c48e1
@ -1,7 +1,7 @@
|
||||
from trezor.wire import register, protobuf_workflow
|
||||
from trezor.messages.wire_types import RippleGetAddress
|
||||
from trezor.messages.MessageType import RippleGetAddress
|
||||
from trezor.messages.MessageType import RippleSignTx
|
||||
from .get_address import get_address
|
||||
from .serializer import *
|
||||
|
||||
|
||||
def dispatch_RippleGetAddress(*args, **kwargs):
|
||||
@ -9,5 +9,11 @@ def dispatch_RippleGetAddress(*args, **kwargs):
|
||||
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)
|
||||
|
@ -1,9 +1,25 @@
|
||||
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'
|
||||
|
||||
def address_from_public_key(pubkey: bytes):
|
||||
# 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:
|
||||
@ -23,3 +39,9 @@ def address_from_public_key(pubkey: bytes):
|
||||
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:]
|
||||
|
23
src/apps/ripple/layout.py
Normal file
23
src/apps/ripple/layout.py
Normal file
@ -0,0 +1,23 @@
|
||||
from apps.common.confirm import require_confirm, require_hold_to_confirm
|
||||
from apps.common.display_address import split_address
|
||||
from trezor import ui
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import format_amount
|
||||
from . import helpers
|
||||
|
||||
|
||||
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)
|
156
src/apps/ripple/serialize.py
Normal file
156
src/apps/ripple/serialize.py
Normal file
@ -0,0 +1,156 @@
|
||||
# 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)
|
68
src/apps/ripple/sign_tx.py
Normal file
68
src/apps/ripple/sign_tx.py
Normal file
@ -0,0 +1,68 @@
|
||||
from apps.common import seed
|
||||
from trezor.crypto import der
|
||||
from trezor.crypto.curve import secp256k1
|
||||
from trezor.crypto.hashlib import sha512
|
||||
from trezor.messages.RippleSignTx import RippleSignTx
|
||||
from trezor.messages.RippleSignedTx import RippleSignedTx
|
||||
from trezor.wire import ProcessError
|
||||
from .serialize import serialize
|
||||
from . import helpers
|
||||
from . import layout
|
||||
|
||||
|
||||
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)")
|
77
tests/test_apps.ripple.serializer.py
Normal file
77
tests/test_apps.ripple.serializer.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user