UNPKG

ox

Version:

Ethereum Standard Library

521 lines 18 kB
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