Serialization: implement issued currency amounts, pathsets

This commit is contained in:
mDuo13
2018-11-26 20:10:37 -08:00
parent 6bcf83ee1b
commit 3894149859
4 changed files with 201 additions and 10 deletions

View File

@@ -9,6 +9,7 @@ import logging
import re import re
from address import decode_address from address import decode_address
from xrpl_num import IssuedAmount
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
@@ -81,6 +82,11 @@ def bytes_from_uint(i, bits):
return i.to_bytes(bits // 8, byteorder="big", signed=False) return i.to_bytes(bits // 8, byteorder="big", signed=False)
def amount_to_bytes(a): def amount_to_bytes(a):
"""
Serializes an "Amount" type, which can be either XRP or an issued currency:
- XRP: 64 bits; 0, followed by 1 ("is positive"), followed by 62 bit UInt amount
- Issued Currency: 64 bits of amount, followed by
"""
if type(a) == str: if type(a) == str:
# is XRP # is XRP
xrp_amt = int(a) xrp_amt = int(a)
@@ -95,32 +101,42 @@ def amount_to_bytes(a):
if sorted(a.keys()) != ["currency", "issuer", "value"]: if sorted(a.keys()) != ["currency", "issuer", "value"]:
raise ValueError("amount must have currency, value, issuer only (actually had: %s)" % raise ValueError("amount must have currency, value, issuer only (actually had: %s)" %
sorted(a.keys())) sorted(a.keys()))
#TODO: canonicalize mantissa/exponent/etc. of issued currency amount
# https://developers.ripple.com/currency-formats.html#issued-currency-math issued_amt = IssuedAmount(a["value"]).to_bytes()
# temporarily returning all zeroes logger.debug("Issued amount: %s"%issued_amt.hex())
issued_amt = bytes(8)
currency_code = currency_code_to_bytes(a["currency"]) currency_code = currency_code_to_bytes(a["currency"])
return issued_amt + currency_code + decode_address(a["issuer"]) return issued_amt + currency_code + decode_address(a["issuer"])
else: else:
raise ValueError("amount must be XRP string or {currency, value, issuer}") raise ValueError("amount must be XRP string or {currency, value, issuer}")
def issued_amount_as_bytes(strnum):
num = Decimal(strnum)
def tx_type_to_bytes(txtype): def tx_type_to_bytes(txtype):
type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype] type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype]
return type_uint.to_bytes(2, byteorder="big", signed=False) return type_uint.to_bytes(2, byteorder="big", signed=False)
def currency_code_to_bytes(code_string): def currency_code_to_bytes(code_string, xrp_ok=False):
if re.match(r"^[A-Za-z0-9?!@#$%^&*<>(){}\[\]|]{3}$", code_string): if re.match(r"^[A-Za-z0-9?!@#$%^&*<>(){}\[\]|]{3}$", code_string):
# ISO 4217-like code # ISO 4217-like code
if code_string == "XRP": if code_string == "XRP":
if xrp_ok:
# Rare, but when the currency code "XRP" is serialized, it's
# a special-case all zeroes.
logger.debug("Currency code(XRP): "+("0"*40))
return bytes(20)
raise ValueError("issued currency can't be XRP") raise ValueError("issued currency can't be XRP")
code_ascii = code_string.encode("ASCII") code_ascii = code_string.encode("ASCII")
logger.debug("Currency code ASCII: %s"%code_ascii.hex())
# standard currency codes: https://developers.ripple.com/currency-formats.html#standard-currency-codes # standard currency codes: https://developers.ripple.com/currency-formats.html#standard-currency-codes
# 8 bits type code (0x00) # 8 bits type code (0x00)
# 96 bits reserved (0's) # 88 bits reserved (0's)
# 24 bits ASCII # 24 bits ASCII
# 8 bits version (0x00) # 16 bits version (0x00)
# 24 bits reserved (0's) # 24 bits reserved (0's)
return b''.join( ( bytes(13), code_ascii, bytes(4) ) ) return b''.join( ( bytes(12), code_ascii, bytes(5) ) )
elif re.match(r"^[0-9a-fA-F]{40}$", code_string): elif re.match(r"^[0-9a-fA-F]{40}$", code_string):
# raw hex code # raw hex code
return bytes.fromhex(code_string) # requires Python 3.5+ return bytes.fromhex(code_string) # requires Python 3.5+
@@ -197,6 +213,58 @@ def object_to_bytes(obj):
fields_as_bytes.append(field_id("ObjectEndMarker")) fields_as_bytes.append(field_id("ObjectEndMarker"))
return b''.join(fields_as_bytes) return b''.join(fields_as_bytes)
def pathset_to_bytes(pathset):
"""
Serialize a PathSet, which is an array of arrays,
where each inner array represents one possible payment path.
A path consists of "path step" objects in sequence, each with one or
more of "account", "currency", and "issuer" fields, plus (ignored) "type"
and "type_hex" fields which indicate which fields are present.
(We re-create the type field for serialization based on which of the core
3 fields are present.)
"""
if not len(pathset):
raise ValueError("PathSet type must not be empty")
paths_as_bytes = []
for n in range(len(pathset)):
path = path_as_bytes(pathset[n])
logger.debug("Path %d: %s"%(n, path.hex()))
paths_as_bytes.append(path)
if n + 1 == len(pathset): # last path; add an end byte
paths_as_bytes.append(bytes.fromhex("00"))
else: # add a path separator byte
paths_as_bytes.append(bytes.fromhex("ff"))
return b''.join(paths_as_bytes)
def path_as_bytes(path):
"""
Helper function for representing one member of a pathset as a bytes object
"""
if not len(path):
raise ValueError("Path must not be empty")
path_contents = []
for step in path:
step_data = []
type_byte = 0
if "account" in step.keys():
type_byte |= 0x01
step_data.append(decode_address(step["account"]))
if "currency" in step.keys():
type_byte |= 0x10
step_data.append(currency_code_to_bytes(step["currency"], xrp_ok=True))
if "issuer" in step.keys():
type_byte |= 0x20
step_data.append(decode_address(step["issuer"]))
step_data = [bytes_from_uint(type_byte, 8)] + step_data
path_contents += step_data
return b''.join(path_contents)
def field_to_bytes(field_name, field_val): def field_to_bytes(field_name, field_val):
""" """
@@ -221,7 +289,7 @@ def field_to_bytes(field_name, field_val):
"Hash128": hash_to_bytes, "Hash128": hash_to_bytes,
"Hash160": hash_to_bytes, "Hash160": hash_to_bytes,
"Hash256": hash_to_bytes, "Hash256": hash_to_bytes,
# TODO: PathSet "PathSet": pathset_to_bytes,
"STArray": array_to_bytes, "STArray": array_to_bytes,
"STObject": object_to_bytes, "STObject": object_to_bytes,
"UInt8" : lambda x:bytes_from_uint(x, 8), "UInt8" : lambda x:bytes_from_uint(x, 8),
@@ -270,7 +338,7 @@ if __name__ == "__main__":
# "Sequence": 2 # "Sequence": 2
# } # }
with open("test-cases/tx2-nometa.json") as f: with open("test-cases/tx3-nometa.json") as f:
example_tx = json.load(f) example_tx = json.load(f)
serialize_tx(example_tx) serialize_tx(example_tx)

View File

@@ -0,0 +1 @@
1200002200000000240000034A201B009717BE61400000000098968068400000000000000C69D4564B964A845AC0000000000000000000000000555344000000000069D33B18D53385F8A3185516C2EDA5DEDB8AC5C673210379F17CFA0FFD7518181594BE69FE9A10471D6DE1F4055C6D2746AFD6CF89889E74473045022100D55ED1953F860ADC1BC5CD993ABB927F48156ACA31C64737865F4F4FF6D015A80220630704D2BD09C8E99F26090C25F11B28F5D96A1350454402C2CED92B39FFDBAF811469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6831469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6F9EA7C06636C69656E747D077274312E312E31E1F1011201F3B1997562FD742B54D4EBDEA1D6AEA3D4906B8F100000000000000000000000000000000000000000FF014B4E9C06F24296074F7BC48F92A97916C6DC5EA901DD39C650A96EDA48334E70CC4A85B8B2E8502CD310000000000000000000000000000000000000000000

View File

@@ -0,0 +1,57 @@
{
"Account": "rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
"Amount": "10000000",
"Destination": "rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 9902014,
"Memos": [
{
"Memo": {
"MemoData": "7274312E312E31",
"MemoType": "636C69656E74"
}
}
],
"Paths": [
[
{
"account": "rPDXxSZcuVL3ZWoyU82bcde3zwvmShkRyF",
"type": 1,
"type_hex": "0000000000000001"
},
{
"currency": "XRP",
"type": 16,
"type_hex": "0000000000000010"
}
],
[
{
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"type": 1,
"type_hex": "0000000000000001"
},
{
"account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
"type": 1,
"type_hex": "0000000000000001"
},
{
"currency": "XRP",
"type": 16,
"type_hex": "0000000000000010"
}
]
],
"SendMax": {
"currency": "USD",
"issuer": "rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp",
"value": "0.6275558355"
},
"Sequence": 842,
"SigningPubKey": "0379F17CFA0FFD7518181594BE69FE9A10471D6DE1F4055C6D2746AFD6CF89889E",
"TransactionType": "Payment",
"TxnSignature": "3045022100D55ED1953F860ADC1BC5CD993ABB927F48156ACA31C64737865F4F4FF6D015A80220630704D2BD09C8E99F26090C25F11B28F5D96A1350454402C2CED92B39FFDBAF",
"hash": "B521424226FC100A2A802FE20476A5F8426FD3F720176DC5CCCE0D75738CC208"
}

View File

@@ -0,0 +1,65 @@
# Serializes issued currency amounts from string number representations,
# matching the precision of the XRP Ledger.
from decimal import getcontext, Decimal
class IssuedAmount:
MIN_MANTISSA = 10**15
MAX_MANTISSA = 10**16 - 1
MIN_EXP = -96
MAX_EXP = 80
def __init__(self, strnum):
self.context = getcontext()
self.context.prec = 15
self.context.Emin = self.MIN_EXP
self.context.Emax = self.MAX_EXP
self.dec = Decimal(strnum)
def to_bytes(self):
if self.dec.is_zero():
return self.canonical_zero_serial()
# Convert components to integers ---------------------------------------
sign, digits, exp = self.dec.as_tuple()
mantissa = int("".join([str(d) for d in digits]))
# Canonicalize to expected range ---------------------------------------
while mantissa < self.MIN_MANTISSA and exp > self.MIN_EXP:
mantissa *= 10
exp -= 1
while mantissa > self.MAX_MANTISSA:
if exp >= self.MAX_EXP:
raise ValueError("amount overflow")
mantissa //= 10
exp += 1
if exp < self.MIN_EXP or mantissa < self.MIN_MANTISSA:
# Round to zero
return self.canonical_zero_serial()
if exp > self.MAX_EXP or mantissa > self.MAX_MANTISSA:
raise ValueError("amount overflow")
# Convert to bytes -----------------------------------------------------
serial = 0x8000000000000000 # "Not XRP" bit set
if sign == 0:
serial |= 0x4000000000000000 # "Is positive" bit set
serial |= ((exp+97) << 54) # next 8 bits are exponent
serial |= mantissa # last 54 bits are mantissa
return serial.to_bytes(8, byteorder="big", signed=False)
def canonical_zero_serial(self):
"""
Returns canonical format for zero:
- "Not XRP" bit = 1
- "Sign bit" = 1 (for positive !!)
- exponent ???
"""
# Mantissa is all zeroes. Must be positive zero.
#bitval = 0b1100000001000000000000000000000000000000000000000000000000000000
#return bitval.to_bytes(8, byteorder="big", signed=False)
return (0x8000000000000000).to_bytes(8, byteorder="big", signed=False)