Binary Format updates w/ new types

- Add Currency, Issue, Number, and additional UInt fields.
- Harmonize type names with updated names from the code
  (for example, Hash128→UInt128)
- Update Python sample code for binary serialization.
- TODO: Add test cases and confirm implementation of new types
- TODO: Update JavaScript sample code.
This commit is contained in:
mDuo13
2025-06-20 17:24:50 -07:00
parent c50e099265
commit 824c335d08
2 changed files with 67 additions and 12 deletions

View File

@@ -1,9 +1,8 @@
#!/usr/bin/env python3
# Transaction Serialization Sample Code (Python3 version)
# Transaction Serialization Sample Code (Python version)
# Author: rome@ripple.com
# Copyright Ripple 2018
# Requires Python 3.5+ because of bytes.hex()
# Copyright Ripple 2018-2025
import argparse
import json
@@ -135,9 +134,11 @@ def accountid_to_bytes(address):
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 160 bit currency code and
- XRP: total 64 bits: 0, followed by 1 ("is positive"), then 0, then 61 bit UInt amount
- Issued Currency: total 384 bits: 64 bits of amount, followed by 160 bit currency code and
160 bit issuer AccountID.
- MPT: 8-bit header with the binary value 01100000 (0x60), then 64 bit UInt amount,
32 bit Sequence number, and 160 bit issuer AccountID.
"""
if type(a) == str:
# is XRP
@@ -194,6 +195,13 @@ def blob_to_bytes(field_val):
vl_contents = bytes.fromhex(field_val)
return vl_encode(vl_contents)
def currency_to_bytes(currency):
"""
Serializes a Currency-type field, which can be either 3-character string or
160-bit hexadecimal.
"""
return currency_code_to_bytes(currency, xrp_ok=True)
def currency_code_to_bytes(code_string, xrp_ok=False):
if re.match(r"^[A-Za-z0-9?!@#$%^&*<>(){}\[\]|]{3}$", code_string):
# ISO 4217-like code
@@ -216,7 +224,7 @@ def currency_code_to_bytes(code_string, xrp_ok=False):
return b''.join( ( bytes(12), code_ascii, bytes(5) ) )
elif re.match(r"^[0-9a-fA-F]{40}$", code_string):
# raw hex code
return bytes.fromhex(code_string) # requires Python 3.5+
return bytes.fromhex(code_string)
else:
raise ValueError("invalid currency code")
@@ -224,30 +232,54 @@ def hash128_to_bytes(contents):
"""
Serializes a hexadecimal string as binary and confirms that it's 128 bits
"""
b = hash_to_bytes(contents)
b = hex_to_bytes(contents)
if len(b) != 16: # 16 bytes = 128 bits
raise ValueError("Hash128 is not 128 bits long")
return b
def hash160_to_bytes(contents):
b = hash_to_bytes(contents)
b = hex_to_bytes(contents)
if len(b) != 20: # 20 bytes = 160 bits
raise ValueError("Hash160 is not 160 bits long")
return b
def hash256_to_bytes(contents):
b = hash_to_bytes(contents)
b = hex_to_bytes(contents)
if len(b) != 32: # 32 bytes = 256 bits
raise ValueError("Hash256 is not 256 bits long")
return b
def hash_to_bytes(contents):
def hex_to_bytes(contents):
"""
Helper function; serializes a hash value from a hexadecimal string
of any length.
"""
return bytes.fromhex(field_val)
def issue_to_bytes(issue):
"""
Serialize an Issue-type field, which defines a fungible token or XRP
without a quantity.
"""
if type(issue) != dict:
raise ValueError("Issue field must be provided as dictionary")
if len(issue.keys()) == 1 and issue.get("currency") != "XRP":
raise ValueError("Issue field must provide currency and issuer unless currency is XRP")
elif sorted(issue.keys()) != ["currency", "issuer"]:
raise ValueError("Issue field must provide currency and issuer unless currency is XRP")
currency_code = currency_code_to_bytes(issue["currency"])
address = decode_address(issue["issuer"])
return currency_code + address
def number_to_bytes(str_num):
"""
Serialize a Number-type field, which is a stand-alone quantity in the same
floating point format as a fungible token amount.
"""
amt = IssuedAmount(str_num)
return amt.to_bytes()
def object_to_bytes(obj):
"""
Serialize an object from decoded JSON.
@@ -349,6 +381,18 @@ def uint16_to_bytes(i):
def uint32_to_bytes(i):
return i.to_bytes(4, byteorder="big", signed=False)
def uint64_to_bytes(i):
# Unlike smaller UInts, UInt64 is serialized as hex in JSON
b = hex_to_bytes(i)
if len(b) != 8: # 8 bytes = 64 bits
raise ValueError("UInt64 is not 64 bits long")
return b
def uint384_to_bytes(i):
b = hex_to_bytes(i)
if len(b) != 8: # 8 bytes = 64 bits
raise ValueError("UInt64 is not 64 bits long")
return b
# Core serialization logic -----------------------------------------------------
@@ -372,15 +416,20 @@ def field_to_bytes(field_name, field_val):
"AccountID": accountid_to_bytes,
"Amount": amount_to_bytes,
"Blob": blob_to_bytes,
"Hash128": hash128_to_bytes,
"Currency": currency_to_bytes,
"Hash128": hash128_to_bytes, # aka UInt128
"Hash160": hash160_to_bytes,
"Hash256": hash256_to_bytes,
"Issue": issue_to_bytes,
"Number": number_to_bytes,
"PathSet": pathset_to_bytes,
"STArray": array_to_bytes,
"STObject": object_to_bytes,
"UInt8" : uint8_to_bytes,
"UInt16": uint16_to_bytes,
"UInt32": uint32_to_bytes,
"UInt64": uint64_to_bytes,
"UInt384": uint384_to_bytes,
}
field_binary = dispatch[field_type](field_val)
return b''.join( (id_prefix, field_binary) )

View File

@@ -196,6 +196,8 @@ Transactions and ledger entries may contain fields of any of the following types
| [UInt160][] | 17 | 160 | No | A 160-bit binary value. This may define a currency code or issuer. |
| [UInt192][] | 21 | 192 | No | A 192-bit binary value. This usually represents an MPT issuance. |
| [UInt256][] | 5 | 256 | No | A 256-bit binary value. This usually represents the hash of a transaction, ledger version, or ledger entry. |
| [UInt384][] | 22 | 384 | No | **UNUSED.** A 384-bit binary value. |
| [UInt512][] | 23 | 512 | No | **UNUSED.** A 512-bit binary value. |
| [Vector256][] | 19 | Variable | Yes | A list of 256-bit binary values. This may be a list of ledger entries or other hash values. |
| [XChainBridge][] | 25 | Variable | No | A bridge between two blockchains, identified by the door accounts and issued assets on both chains. |
@@ -391,11 +393,15 @@ The following example shows the serialization format for a PathSet:
[UInt128]: #uint-fields
[UInt160]: #uint-fields
[UInt256]: #uint-fields
[UInt384]: #uint-fields
[UInt512]: #uint-fields
The XRP Ledger has several unsigned integer types: UInt8, UInt16, UInt32, UInt64, UInt128, UInt160, and UInt256. All of these are standard big-endian binary unsigned integers with the specified number of bits. The larger types such as UInt128, UInt160, and UInt256 were previously named Hash128, Hash160, and Hash256 because they often contain hash function outputs.
The XRP Ledger has several unsigned integer types: UInt8, UInt16, UInt32, UInt64, UInt128, UInt160, and UInt256. All of these are standard big-endian binary unsigned integers with the specified number of bits. The larger types such as UInt128, UInt160, and UInt256 were previously named `Hash128`, `Hash160`, and `Hash256` because they often contain hash function outputs. (These names are still used in the definitions file.)
When representing these fields in JSON, these fields may be represented as JSON numbers, strings containing hexadecimal, or as strings containing decimal numbers, depending on the bit size and intended use of the data. UInt64 and up are never converted to JSON numbers, because some JSON decoders may try to represent them as "double precision" floating point numbers, which cannot represent all distinct UInt64 values with full precision. UInt128 and UInt256 typically represent hash values or arbitrary data, so they are typically represented in JSON as hexadecimal.
The types UInt96, UInt384, and UInt512 are currently defined but not used.
The `TransactionType` field is a special case. In JSON, this field is conventionally represented as a string with the name of the transaction type. In binary, this field is a UInt16. The `TRANSACTION_TYPES` object in the [definitions file](#definitions-file) maps these strings to the numeric values used in the binary format.
### Vector256 Fields