ox
Version:
Ethereum Standard Library
521 lines • 18 kB
JavaScript
import * as Bytes from './Bytes.js';
import * as Errors from './Errors.js';
import * as Hex from './Hex.js';
import * as internal_bytes from './internal/bytes.js';
import * as Cursor from './internal/cursor.js';
/** Maximum nesting depth permitted when decoding an RLP value. */
const depthLimit = 1_024;
/**
* Decodes a Recursive-Length Prefix (RLP) value into a {@link ox#Bytes.Bytes} value.
*
* @example
* ```ts twoslash
* import { Rlp } from 'ox'
* Rlp.toBytes('0x8b68656c6c6f20776f726c64')
* // Uint8Array([139, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100])
* ```
*
* @param value - The value to decode.
* @returns The decoded {@link ox#Bytes.Bytes} value.
*/
export function toBytes(value) {
return to(value, 'Bytes');
}
/**
* Decodes a Recursive-Length Prefix (RLP) value into a {@link ox#Hex.Hex} value.
*
* @example
* ```ts twoslash
* import { Rlp } from 'ox'
* Rlp.toHex('0x8b68656c6c6f20776f726c64')
* // 0x68656c6c6f20776f726c64
* ```
*
* @param value - The value to decode.
* @returns The decoded {@link ox#Hex.Hex} value.
*/
export function toHex(value) {
return to(value, 'Hex');
}
/////////////////////////////////////////////////////////////////////////////////
// Internal
/////////////////////////////////////////////////////////////////////////////////
/** @internal */
export function to(value, to) {
const to_ = to ?? (typeof value === 'string' ? 'Hex' : 'Bytes');
const bytes = typeof value === 'string' ? Bytes.fromHex(value) : value;
const cursor = Cursor.create(bytes, {
recursiveReadLimit: Number.POSITIVE_INFINITY,
});
const result = decodeRlpCursor(cursor, to_);
// RLP payloads encode exactly one item (Yellow Paper, Appendix B).
if (cursor.position < cursor.bytes.length)
throw new TrailingBytesError({
count: cursor.bytes.length - cursor.position,
});
return result;
}
/** @internal */
/** @internal */
export function decodeRlpCursor(cursor, to = 'Hex', depth = 0) {
if (depth >= depthLimit)
throw new DepthLimitExceededError({ limit: depthLimit });
if (cursor.bytes.length === 0)
return (to === 'Hex' ? Hex.fromBytes(cursor.bytes) : cursor.bytes);
const prefix = cursor.readByte();
if (prefix < 0x80)
cursor.decrementPosition(1);
// bytes
if (prefix < 0xc0) {
const length = readLength(cursor, prefix, 0x80);
const bytes = cursor.readBytes(length);
return (to === 'Hex' ? Hex.fromBytes(bytes) : bytes);
}
// list
const length = readLength(cursor, prefix, 0xc0);
return readList(cursor, length, to, depth + 1);
}
/** @internal */
export function readLength(cursor, prefix, offset) {
if (offset === 0x80 && prefix < 0x80)
return 1;
if (prefix <= offset + 55)
return prefix - offset;
if (prefix === offset + 55 + 1)
return cursor.readUint8();
if (prefix === offset + 55 + 2)
return cursor.readUint16();
if (prefix === offset + 55 + 3)
return cursor.readUint24();
if (prefix === offset + 55 + 4)
return cursor.readUint32();
throw new Errors.BaseError('Invalid RLP prefix');
}
/** @internal */
export function readList(cursor, length, to, depth = 0) {
const position = cursor.position;
const value = [];
while (cursor.position - position < length)
value.push(decodeRlpCursor(cursor, to, depth));
// Items must consume exactly the declared list length.
if (cursor.position - position !== length)
throw new ListBoundaryExceededError({
consumed: cursor.position - position,
declared: length,
});
return value;
}
/**
* Encodes a {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value into a Recursive-Length Prefix (RLP) value.
*
* @example
* ```ts twoslash
* import { Bytes, Rlp } from 'ox'
*
* Rlp.from('0x68656c6c6f20776f726c64', { as: 'Hex' })
* // @log: 0x8b68656c6c6f20776f726c64
*
* Rlp.from(
* Bytes.from([
* 139, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108,
* 100
* ]),
* { as: 'Bytes' }
* )
* // @log: Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100])
* ```
*
* @param value - The {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value to encode.
* @param options - Options.
* @returns The RLP value.
*/
export function from(value, options) {
const { as } = options;
// Two-walk encode without the per-node `Encodable` closure tree:
// 1. `measure` walks the input once and caches each list's `bodyLength`
// in a side array indexed by visit order. This makes the second walk
// O(N) instead of O(N²) for nested inputs.
// 2. `writeEncoded` walks again, reads cached body lengths, and writes
// bytes straight into the pre-sized buffer. Hex leaves are
// nibble-decoded directly into the destination, skipping the per-leaf
// `Bytes.fromHex` allocation.
const ctx = { lengths: [], cursor: 0 };
const totalLength = measure(value, ctx);
// Hex-output fast path: when the caller asked for hex AND every leaf is
// already hex, emit a hex string directly instead of allocating an
// intermediate `Uint8Array` and round-tripping through `Hex.fromBytes`.
// This is the dominant shape for transaction envelope serialize.
if (as === 'Hex' && isAllHex(value)) {
const parts = [];
writeEncodedHex(parts, value, {
lengths: ctx.lengths,
cursor: 0,
});
return `0x${parts.join('')}`;
}
const bytes = new Uint8Array(totalLength);
writeEncoded(bytes, 0, value, { lengths: ctx.lengths, cursor: 0 });
if (as === 'Hex')
return Hex.fromBytes(bytes);
return bytes;
}
/**
* Encodes a {@link ox#Bytes.Bytes} value into a Recursive-Length Prefix (RLP) value.
*
* @example
* ```ts twoslash
* import { Bytes, Rlp } from 'ox'
*
* Rlp.fromBytes(
* Bytes.from([
* 139, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108,
* 100
* ])
* )
* // @log: Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100])
* ```
*
* @param bytes - The {@link ox#Bytes.Bytes} value to encode.
* @param options - Options.
* @returns The RLP value.
*/
export function fromBytes(bytes, options = {}) {
const { as = 'Bytes' } = options;
return from(bytes, { as });
}
/**
* Encodes a {@link ox#Hex.Hex} value into a Recursive-Length Prefix (RLP) value.
*
* @example
* ```ts twoslash
* import { Rlp } from 'ox'
*
* Rlp.fromHex('0x68656c6c6f20776f726c64')
* // @log: 0x8b68656c6c6f20776f726c64
* ```
*
* @param hex - The {@link ox#Hex.Hex} value to encode.
* @param options - Options.
* @returns The RLP value.
*/
export function fromHex(hex, options = {}) {
const { as = 'Hex' } = options;
return from(hex, { as });
}
/////////////////////////////////////////////////////////////////////////////////
// Internal
/////////////////////////////////////////////////////////////////////////////////
/**
* Returns the byte length needed to encode `length` itself (1-4 bytes per
* RLP), or throws when `length` exceeds the protocol cap.
*
* @internal
*/
function getSizeOfLength(length) {
if (length <= 0xff)
return 1;
if (length <= 0xff_ff)
return 2;
if (length <= 0xff_ff_ff)
return 3;
if (length <= 0xff_ff_ff_ff)
return 4;
throw new Errors.BaseError('Length is too large.');
}
/**
* Walks `value` once, caches each list's `bodyLength` into `ctx.lengths`,
* and returns the total encoded byte length. Allocates nothing per node
* beyond the shared `lengths` array entries.
*
* @internal
*/
function measure(value, ctx) {
if (Array.isArray(value)) {
// Reserve this list's slot before descending so children's slots come
// after ours; `writeEncoded` walks in the same order and reads slot N
// when it visits the Nth list.
const slot = ctx.lengths.length;
ctx.lengths.push(0);
let bodyLength = 0;
for (let i = 0; i < value.length; i++)
bodyLength += measure(value[i], ctx);
ctx.lengths[slot] = bodyLength;
if (bodyLength <= 55)
return 1 + bodyLength;
return 1 + getSizeOfLength(bodyLength) + bodyLength;
}
// Hex leaf: byte length = ceil((hex.length - 2) / 2). The `>> 1` of
// `length - 1` yields ceil for both odd- and even-nibble inputs.
if (typeof value === 'string') {
const byteLen = (value.length - 1) >> 1;
if (byteLen === 0)
return 1;
if (byteLen === 1) {
// Single-byte values < 0x80 encode as themselves (no prefix).
const odd = (value.length & 1) === 1;
const firstChar = value.charCodeAt(odd ? 2 : 3);
const high = odd
? 0
: (internal_bytes.charCodeToBase16(value.charCodeAt(2)) ?? 0) << 4;
const low = internal_bytes.charCodeToBase16(firstChar) ?? 0;
const byte = high | low;
if (byte < 0x80)
return 1;
return 2;
}
if (byteLen <= 55)
return 1 + byteLen;
return 1 + getSizeOfLength(byteLen) + byteLen;
}
// Bytes leaf
const len = value.length;
if (len === 1 && value[0] < 0x80)
return 1;
if (len <= 55)
return 1 + len;
return 1 + getSizeOfLength(len) + len;
}
/**
* Writes `value`'s RLP encoding into `bytes` starting at `offset` and returns
* the next free offset. Reads list body lengths from `ctx.lengths` in the
* same DFS order that `measure` filled them. Hex leaves are nibble-decoded
* directly into the destination, skipping the per-leaf `Bytes.fromHex`
* allocation.
*
* @internal
*/
function writeEncoded(bytes, offset, value, ctx) {
if (Array.isArray(value)) {
const bodyLength = ctx.lengths[ctx.cursor++];
let cursor = offset;
if (bodyLength <= 55) {
bytes[cursor++] = 0xc0 + bodyLength;
}
else {
const sizeOfBodyLength = getSizeOfLength(bodyLength);
bytes[cursor++] = 0xc0 + 55 + sizeOfBodyLength;
cursor = writeBigEndian(bytes, cursor, bodyLength, sizeOfBodyLength);
}
for (let i = 0; i < value.length; i++)
cursor = writeEncoded(bytes, cursor, value[i], ctx);
return cursor;
}
if (typeof value === 'string')
return writeHexLeaf(bytes, offset, value);
return writeBytesLeaf(bytes, offset, value);
}
/**
* Hex-leaf fast path: writes the RLP encoding of a hex string directly into
* `bytes` by nibble-decoding the source hex chars into the destination
* buffer. Even-pads odd-nibble hex (e.g. `'0x1'`) on the fly.
*
* @internal
*/
function writeHexLeaf(bytes, offset, hex) {
const dataStart = 2;
let byteLen = (hex.length - 2) >> 1;
let highNibbleFromOddPad = false;
if ((hex.length & 1) === 1) {
// Odd-nibble: first emitted byte's high nibble is `0` (left-pad).
byteLen += 1;
highNibbleFromOddPad = true;
}
// Empty leaf -> single 0x80 prefix byte (zero-length string in RLP).
if (byteLen === 0) {
bytes[offset] = 0x80;
return offset + 1;
}
// Single-byte fast path: if the byte < 0x80, write it as-is (no prefix).
if (byteLen === 1) {
let byte;
if (highNibbleFromOddPad) {
byte = internal_bytes.charCodeToBase16(hex.charCodeAt(dataStart)) ?? 0;
}
else {
const high = internal_bytes.charCodeToBase16(hex.charCodeAt(dataStart)) ?? 0;
const low = internal_bytes.charCodeToBase16(hex.charCodeAt(dataStart + 1)) ?? 0;
byte = (high << 4) | low;
}
if (byte < 0x80) {
bytes[offset] = byte;
return offset + 1;
}
bytes[offset] = 0x80 + 1;
bytes[offset + 1] = byte;
return offset + 2;
}
let dest = offset;
if (byteLen <= 55) {
bytes[dest++] = 0x80 + byteLen;
}
else {
const sizeOfBytesLength = getSizeOfLength(byteLen);
bytes[dest++] = 0x80 + 55 + sizeOfBytesLength;
dest = writeBigEndian(bytes, dest, byteLen, sizeOfBytesLength);
}
// Decode hex nibbles directly into the destination buffer.
let src = dataStart;
if (highNibbleFromOddPad) {
const low = internal_bytes.charCodeToBase16(hex.charCodeAt(src++));
if (low === undefined)
throw invalidNibble(hex);
bytes[dest++] = low;
}
while (src < hex.length) {
const high = internal_bytes.charCodeToBase16(hex.charCodeAt(src++));
const low = internal_bytes.charCodeToBase16(hex.charCodeAt(src++));
if (high === undefined || low === undefined)
throw invalidNibble(hex);
bytes[dest++] = (high << 4) | low;
}
return dest;
}
function writeBytesLeaf(bytes, offset, leaf) {
const len = leaf.length;
if (len === 1 && leaf[0] < 0x80) {
bytes[offset] = leaf[0];
return offset + 1;
}
let dest = offset;
if (len <= 55) {
bytes[dest++] = 0x80 + len;
}
else {
const sizeOfBytesLength = getSizeOfLength(len);
bytes[dest++] = 0x80 + 55 + sizeOfBytesLength;
dest = writeBigEndian(bytes, dest, len, sizeOfBytesLength);
}
bytes.set(leaf, dest);
return dest + len;
}
/**
* Returns true if every leaf in the (possibly nested) input is a hex string.
* Used to gate the hex-output fast path in `from`.
*
* @internal
*/
function isAllHex(value) {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++)
if (!isAllHex(value[i]))
return false;
return true;
}
return typeof value === 'string';
}
/**
* Hex-output fast path: writes the RLP encoding of `value` directly as hex
* substrings into `parts`. Avoids the intermediate `Uint8Array` allocation
* (and the trailing `Hex.fromBytes` round-trip) used by the bytes path.
*
* Length-prefix bytes are formatted via the cached `hexes[]` table so we
* never call `toString(16)` per node.
*
* @internal
*/
function writeEncodedHex(parts, value, ctx) {
if (Array.isArray(value)) {
const bodyLength = ctx.lengths[ctx.cursor++];
if (bodyLength <= 55) {
parts.push(hexes[0xc0 + bodyLength]);
}
else {
const sizeOfBodyLength = getSizeOfLength(bodyLength);
parts.push(hexes[0xc0 + 55 + sizeOfBodyLength]);
parts.push(bigEndianHex(bodyLength, sizeOfBodyLength));
}
for (let i = 0; i < value.length; i++)
writeEncodedHex(parts, value[i], ctx);
return;
}
// Hex leaf: even-pad odd-nibble inputs and skip the `0x` prefix.
const hex = value;
const odd = (hex.length & 1) === 1;
const body = odd ? `0${hex.slice(2)}` : hex.slice(2);
const byteLen = body.length >> 1;
if (byteLen === 0) {
parts.push(hexes[0x80]);
return;
}
if (byteLen === 1) {
const byte = parseInt(body, 16);
if (byte < 0x80) {
parts.push(body);
}
else {
parts.push(hexes[0x81]);
parts.push(body);
}
return;
}
if (byteLen <= 55) {
parts.push(hexes[0x80 + byteLen]);
parts.push(body);
return;
}
const sizeOfBytesLength = getSizeOfLength(byteLen);
parts.push(hexes[0x80 + 55 + sizeOfBytesLength]);
parts.push(bigEndianHex(byteLen, sizeOfBytesLength));
parts.push(body);
}
const hexes = /*#__PURE__*/ Array.from({ length: 256 }, (_v, i) => i.toString(16).padStart(2, '0'));
/**
* Returns the big-endian hex encoding of `value` in `size` bytes.
*
* @internal
*/
function bigEndianHex(value, size) {
if (size === 1)
return hexes[value & 0xff];
if (size === 2)
return `${hexes[(value >>> 8) & 0xff]}${hexes[value & 0xff]}`;
if (size === 3)
return `${hexes[(value >>> 16) & 0xff]}${hexes[(value >>> 8) & 0xff]}${hexes[value & 0xff]}`;
return `${hexes[(value >>> 24) & 0xff]}${hexes[(value >>> 16) & 0xff]}${hexes[(value >>> 8) & 0xff]}${hexes[value & 0xff]}`;
}
function writeBigEndian(bytes, offset, value, size) {
if (size === 1) {
bytes[offset] = value & 0xff;
}
else if (size === 2) {
bytes[offset] = (value >>> 8) & 0xff;
bytes[offset + 1] = value & 0xff;
}
else if (size === 3) {
bytes[offset] = (value >>> 16) & 0xff;
bytes[offset + 1] = (value >>> 8) & 0xff;
bytes[offset + 2] = value & 0xff;
}
else {
bytes[offset] = (value >>> 24) & 0xff;
bytes[offset + 1] = (value >>> 16) & 0xff;
bytes[offset + 2] = (value >>> 8) & 0xff;
bytes[offset + 3] = value & 0xff;
}
return offset + size;
}
function invalidNibble(hex) {
return new Errors.BaseError(`Invalid hex string \`${hex}\`.`);
}
/** Thrown when an RLP value nests deeper than the decode depth limit. */
export class DepthLimitExceededError extends Errors.BaseError {
name = 'Rlp.DepthLimitExceededError';
constructor({ limit }) {
super(`RLP depth limit of \`${limit}\` exceeded.`);
}
}
/** Thrown when RLP list items overrun the list's declared length. */
export class ListBoundaryExceededError extends Errors.BaseError {
name = 'Rlp.ListBoundaryExceededError';
constructor({ consumed, declared }) {
super(`RLP list items consumed \`${consumed}\` bytes but the list declared a length of \`${declared}\`.`);
}
}
/** Thrown when an RLP payload contains bytes after the decoded item. */
export class TrailingBytesError extends Errors.BaseError {
name = 'Rlp.TrailingBytesError';
constructor({ count }) {
super(`RLP payload encodes a single item, but \`${count}\` trailing ${count === 1 ? 'byte remains' : 'bytes remain'}.`);
}
}
//# sourceMappingURL=Rlp.js.map