UNPKG

ox

Version:

Ethereum Standard Library

441 lines (410 loc) 14.7 kB
import { Bytes, Hex, Rlp } from 'ox' import { describe, expect, test } from 'vp/test' test('exports', () => { expect(Object.keys(Rlp)).toMatchInlineSnapshot(` [ "toBytes", "toHex", "to", "decodeRlpCursor", "readLength", "readList", "from", "fromBytes", "fromHex", "DepthLimitExceededError", "ListBoundaryExceededError", "TrailingBytesError", ] `) }) const generateBytes = (length: number) => { const bytes = new Uint8Array(length) for (let i = 0; i < length; i++) bytes[i] = i return bytes } const generateList = (length: number) => { const bytes: Uint8Array[] = [] for (let i = 0; i < length; i++) bytes.push(generateBytes(i % 8)) return bytes } describe('Rlp.to', () => { test('no bytes', () => { const rlpHex = '0x' const rlpBytes = Uint8Array.from([]) // hex -> bytes expect(Rlp.toBytes(rlpHex)).toStrictEqual(new Uint8Array([])) // hex -> hex expect(Rlp.toHex(rlpHex)).toEqual('0x') // bytes -> bytes expect(Rlp.toBytes(rlpBytes)).toStrictEqual(new Uint8Array([])) // bytes -> hex expect(Rlp.toHex(rlpBytes)).toEqual('0x') }) describe('prefix < 0x80', () => { test('bytes -> bytes', () => { expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x00')))).toEqual( Bytes.fromHex('0x00'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x01')))).toEqual( Bytes.fromHex('0x01'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x42')))).toEqual( Bytes.fromHex('0x42'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromHex('0x7f')))).toEqual( Bytes.fromHex('0x7f'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromString('!')))).toEqual( Bytes.fromHex('0x21'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromString('a')))).toEqual( Bytes.fromHex('0x61'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromString('~')))).toEqual( Bytes.fromHex('0x7e'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromBoolean(true)))).toEqual( Bytes.fromHex('0x01'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromBoolean(false)))).toEqual( Bytes.fromHex('0x00'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromNumber(0)))).toEqual( Bytes.fromHex('0x00'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromNumber(69)))).toEqual( Bytes.fromHex('0x45'), ) expect(Rlp.toBytes(Rlp.fromBytes(Bytes.fromNumber(127)))).toEqual( Bytes.fromHex('0x7f'), ) }) test('bytes -> hex', () => { expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x00'), { as: 'Hex' }))).toEqual( '0x00', ) expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x01'), { as: 'Hex' }))).toEqual( '0x01', ) expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x42'), { as: 'Hex' }))).toEqual( '0x42', ) expect(Rlp.toHex(Rlp.from(Bytes.fromHex('0x7f'), { as: 'Hex' }))).toEqual( '0x7f', ) expect(Rlp.toHex(Rlp.from(Bytes.fromString('!'), { as: 'Hex' }))).toEqual( '0x21', ) expect(Rlp.toHex(Rlp.from(Bytes.fromString('a'), { as: 'Hex' }))).toEqual( '0x61', ) expect(Rlp.toHex(Rlp.from(Bytes.fromString('~'), { as: 'Hex' }))).toEqual( '0x7e', ) expect( Rlp.toHex(Rlp.from(Bytes.fromBoolean(true), { as: 'Hex' })), ).toEqual('0x01') expect( Rlp.toHex(Rlp.from(Bytes.fromBoolean(false), { as: 'Hex' })), ).toEqual('0x00') expect(Rlp.toHex(Rlp.from(Bytes.fromNumber(0), { as: 'Hex' }))).toEqual( '0x00', ) expect(Rlp.toHex(Rlp.from(Bytes.fromNumber(69), { as: 'Hex' }))).toEqual( '0x45', ) expect(Rlp.toHex(Rlp.from(Bytes.fromNumber(127), { as: 'Hex' }))).toEqual( '0x7f', ) }) test('hex -> hex', () => { expect(Rlp.toHex(Rlp.from('0x00', { as: 'Hex' }))).toEqual('0x00') expect(Rlp.toHex(Rlp.from('0x01', { as: 'Hex' }))).toEqual('0x01') expect(Rlp.toHex(Rlp.from('0x42', { as: 'Hex' }))).toEqual('0x42') expect(Rlp.toHex(Rlp.fromHex('0x7f'))).toEqual('0x7f') expect(Rlp.toHex(Rlp.fromHex(Hex.fromString('!')))).toEqual('0x21') expect(Rlp.toHex(Rlp.fromHex(Hex.fromString('a')))).toEqual('0x61') expect(Rlp.toHex(Rlp.fromHex(Hex.fromString('~')))).toEqual('0x7e') expect(Rlp.toHex(Rlp.fromHex(Hex.fromBoolean(true)))).toEqual('0x01') expect(Rlp.toHex(Rlp.fromHex(Hex.fromBoolean(false)))).toEqual('0x00') expect(Rlp.toHex(Rlp.fromHex(Hex.fromNumber(0)))).toEqual('0x00') expect(Rlp.toHex(Rlp.fromHex(Hex.fromNumber(69)))).toEqual('0x45') expect(Rlp.toHex(Rlp.fromHex(Hex.fromNumber(127)))).toEqual('0x7f') }) test('hex -> bytes', () => { expect(Rlp.toBytes(Rlp.from('0x00', { as: 'Bytes' }))).toEqual( Bytes.fromHex('0x00'), ) expect(Rlp.toBytes(Rlp.from('0x01', { as: 'Bytes' }))).toEqual( Bytes.fromHex('0x01'), ) expect(Rlp.toBytes(Rlp.from('0x42', { as: 'Bytes' }))).toEqual( Bytes.fromHex('0x42'), ) expect(Rlp.toBytes(Rlp.from('0x7f', { as: 'Bytes' }))).toEqual( Bytes.fromHex('0x7f'), ) expect( Rlp.toBytes(Rlp.from(Hex.fromString('!'), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x21')) expect( Rlp.toBytes(Rlp.from(Hex.fromString('a'), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x61')) expect( Rlp.toBytes(Rlp.from(Hex.fromString('~'), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x7e')) expect( Rlp.toBytes(Rlp.from(Hex.fromBoolean(true), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x01')) expect( Rlp.toBytes(Rlp.from(Hex.fromBoolean(false), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x00')) expect(Rlp.toBytes(Rlp.from(Hex.fromNumber(0), { as: 'Bytes' }))).toEqual( Bytes.fromHex('0x00'), ) expect( Rlp.toBytes(Rlp.from(Hex.fromNumber(69), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x45')) expect( Rlp.toBytes(Rlp.from(Hex.fromNumber(127), { as: 'Bytes' })), ).toEqual(Bytes.fromHex('0x7f')) }) }) describe('list', () => { test('no bytes', () => { // bytes -> bytes expect(Rlp.toBytes(Rlp.fromBytes([]))).toEqual([]) // bytes -> hex expect(Rlp.toHex(Rlp.fromBytes([]))).toEqual([]) // hex -> hex expect(Rlp.toHex(Rlp.fromHex([]))).toEqual([]) // hex -> bytes expect(Rlp.toBytes(Rlp.fromHex([]))).toEqual([]) }) test('inner no bytes', () => { // bytes -> bytes expect(Rlp.toBytes(Rlp.fromBytes([[]]))).toEqual([[]]) // bytes -> hex expect(Rlp.toHex(Rlp.fromBytes([[]]))).toEqual([[]]) // hex -> hex expect(Rlp.toHex(Rlp.fromHex([[]]))).toEqual([[]]) // hex -> bytes expect(Rlp.toBytes(Rlp.fromHex([[]]))).toEqual([[]]) }) describe('prefix < 0xf8', () => { test('bytes -> bytes', () => { expect(Rlp.toBytes(Rlp.fromBytes([Bytes.fromHex('0x00')]))).toEqual([ Bytes.fromHex('0x00'), ]) expect(Rlp.toBytes(Rlp.fromBytes([Bytes.fromHex('0x80')]))).toEqual([ Bytes.fromHex('0x80'), ]) expect(Rlp.toBytes(Rlp.fromBytes(generateList(14)))).toEqual( generateList(14), ) expect( Rlp.toBytes( Rlp.fromBytes([ generateList(4), [generateList(8), [generateList(3), generateBytes(1)]], ]), ), ).toEqual([ generateList(4), [generateList(8), [generateList(3), generateBytes(1)]], ]) }) test('bytes -> hex', () => { expect(Rlp.toHex(Rlp.from(['0x00'], { as: 'Bytes' }))).toEqual(['0x00']) expect(Rlp.toHex(Rlp.from(['0x80'], { as: 'Bytes' }))).toEqual(['0x80']) expect(Rlp.toHex(Rlp.from(generateList(14), { as: 'Bytes' }))).toEqual( generateList(14).map((x) => Hex.fromBytes(x)), ) expect( Rlp.toHex( Rlp.from( [ generateList(4), [generateList(8), [generateList(3), generateBytes(1)]], ], { as: 'Bytes' }, ), ), ).toEqual([ generateList(4).map((x) => Hex.fromBytes(x)), [ generateList(8).map((x) => Hex.fromBytes(x)), [ generateList(3).map((x) => Hex.fromBytes(x)), Hex.fromBytes(generateBytes(1)), ], ], ]) }) test('hex -> hex', () => { expect(Rlp.toHex(Rlp.fromHex(['0x00']))).toEqual(['0x00']) expect(Rlp.toHex(Rlp.fromHex(['0x80']))).toEqual(['0x80']) expect( Rlp.toHex(Rlp.fromHex(generateList(14).map((x) => Hex.fromBytes(x)))), ).toEqual(generateList(14).map((x) => Hex.fromBytes(x))) expect( Rlp.toHex( Rlp.fromHex([ generateList(4).map((x) => Hex.fromBytes(x)), [ generateList(8).map((x) => Hex.fromBytes(x)), [ generateList(3).map((x) => Hex.fromBytes(x)), Hex.fromBytes(generateBytes(1)), ], ], ]), ), ).toEqual([ generateList(4).map((x) => Hex.fromBytes(x)), [ generateList(8).map((x) => Hex.fromBytes(x)), [ generateList(3).map((x) => Hex.fromBytes(x)), Hex.fromBytes(generateBytes(1)), ], ], ]) }) test('hex -> bytes', () => { expect(Rlp.toBytes(Rlp.from(['0x00'], { as: 'Hex' }))).toEqual([ Bytes.fromHex('0x00'), ]) expect(Rlp.toBytes(Rlp.from(['0x80'], { as: 'Hex' }))).toEqual([ Bytes.fromHex('0x80'), ]) expect(Rlp.toBytes(Rlp.from(generateList(14), { as: 'Hex' }))).toEqual( generateList(14), ) expect( Rlp.toBytes( Rlp.from( [ generateList(4), [generateList(8), [generateList(3), generateBytes(1)]], ], { as: 'Hex' }, ), ), ).toEqual([ generateList(4), [generateList(8), [generateList(3), generateBytes(1)]], ]) }) }) test('prefix === 0xf9', () => { expect(Rlp.toBytes(Rlp.fromBytes(generateList(61)))).toEqual( generateList(61), ) expect(Rlp.toBytes(Rlp.fromBytes(generateList(12_000)))).toEqual( generateList(12_000), ) }) test('prefix === 0xfa', () => { expect(Rlp.toBytes(Rlp.fromBytes(generateList(60_000)))).toEqual( generateList(60_000), ) }) }) test('error: invalid hex value', () => { expect(() => Rlp.toHex('0x010')).toThrowErrorMatchingInlineSnapshot( ` [Hex.InvalidLengthError: Hex value \`"0x010"\` is an odd length (3 nibbles). It must be an even length.] `, ) }) }) describe('decode depth limit', () => { // Builds an RLP-encoded value of `depth` nested empty lists without recursing // (so we can exceed the decoder's depth limit without overflowing the encoder). const nestedEmptyLists = (depth: number) => { let bytes = Uint8Array.from([0xc0]) for (let i = 0; i < depth; i++) { const length = bytes.length const prefix = length <= 55 ? [0xc0 + length] : (() => { const lengthBytes: number[] = [] let n = length while (n > 0) { lengthBytes.unshift(n & 0xff) n >>= 8 } return [0xf7 + lengthBytes.length, ...lengthBytes] })() const next = new Uint8Array(prefix.length + bytes.length) next.set(prefix, 0) next.set(bytes, prefix.length) bytes = next } return bytes } test('decodes up to the depth limit', () => { expect(() => Rlp.toBytes(nestedEmptyLists(1_023))).not.toThrow() }) test('throws when the depth limit is exceeded', () => { expect(() => Rlp.toBytes(nestedEmptyLists(2_000)), ).toThrowErrorMatchingInlineSnapshot( `[Rlp.DepthLimitExceededError: RLP depth limit of \`1024\` exceeded.]`, ) }) }) describe('trailing bytes', () => { // RLP payloads encode exactly one item (Yellow Paper, Appendix B). test('after string item', () => { expect(() => Rlp.toHex('0x80deadbeef')).toThrowErrorMatchingInlineSnapshot( `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`4\` trailing bytes remain.]`, ) }) test('after list item', () => { expect(() => Rlp.toHex('0xc2010203')).toThrowErrorMatchingInlineSnapshot( `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`1\` trailing byte remains.]`, ) }) test('after empty list', () => { expect(() => Rlp.toHex('0xc000')).toThrowErrorMatchingInlineSnapshot( `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`1\` trailing byte remains.]`, ) }) test('bytes input', () => { expect(() => Rlp.toHex(Uint8Array.from([0x80, 0xde, 0xad])), ).toThrowErrorMatchingInlineSnapshot( `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`2\` trailing bytes remain.]`, ) }) test('valid single items still decode', () => { expect(Rlp.toHex('0x80')).toEqual('0x') expect(Rlp.toHex('0xc0')).toEqual([]) expect(Rlp.toHex('0xc1c0')).toEqual([[]]) expect(Rlp.toHex('0x83646f67')).toEqual('0x646f67') }) }) describe('list boundary', () => { // Items must consume exactly the declared list length. test('item extends beyond list', () => { // `0xc1` declares 1 payload byte; `0x82aabb` consumes 3. expect(() => Rlp.toHex('0xc182aabb')).toThrowErrorMatchingInlineSnapshot( `[Rlp.ListBoundaryExceededError: RLP list items consumed \`3\` bytes but the list declared a length of \`1\`.]`, ) }) test('item extends beyond nested list', () => { expect(() => Rlp.toHex('0xc3c182aabb')).toThrowErrorMatchingInlineSnapshot( `[Rlp.ListBoundaryExceededError: RLP list items consumed \`3\` bytes but the list declared a length of \`1\`.]`, ) }) test('item extends beyond list with trailing bytes', () => { expect(() => Rlp.toHex('0xc182aabbff')).toThrowErrorMatchingInlineSnapshot( `[Rlp.ListBoundaryExceededError: RLP list items consumed \`3\` bytes but the list declared a length of \`1\`.]`, ) }) })