diff --git a/packages/evm/src/precompiles/01-ecrecover.ts b/packages/evm/src/precompiles/01-ecrecover.ts index 06f4fdadf1..c7ab2e90d3 100644 --- a/packages/evm/src/precompiles/01-ecrecover.ts +++ b/packages/evm/src/precompiles/01-ecrecover.ts @@ -25,7 +25,7 @@ export function precompile01(opts: PrecompileInput): ExecResult { return OOGResult(opts.gasLimit) } - const data = setLengthRight(opts.data, 128) + const data = setLengthRight(opts.data, 128, { allowTruncate: true }) const msgHash = data.subarray(0, 32) const v = data.subarray(32, 64) diff --git a/packages/util/src/binaryTree.ts b/packages/util/src/binaryTree.ts index 5995694840..0b329feb8e 100644 --- a/packages/util/src/binaryTree.ts +++ b/packages/util/src/binaryTree.ts @@ -279,14 +279,14 @@ export function decodeBinaryTreeLeafBasicData( */ export function encodeBinaryTreeLeafBasicData(account: Account): Uint8Array { const encodedVersion = setLengthLeft( - int32ToBytes(account.version), + intToBytes(account.version), BINARY_TREE_VERSION_BYTES_LENGTH, ) // Per EIP-7864, bytes 1-4 are reserved for future use const reservedBytes = new Uint8Array([0, 0, 0, 0]) const encodedNonce = setLengthLeft(bigIntToBytes(account.nonce), BINARY_TREE_NONCE_BYTES_LENGTH) const encodedCodeSize = setLengthLeft( - int32ToBytes(account.codeSize), + intToBytes(account.codeSize), BINARY_TREE_CODE_SIZE_BYTES_LENGTH, ) const encodedBalance = setLengthLeft( diff --git a/packages/util/src/bytes.ts b/packages/util/src/bytes.ts index 705744b78e..28ead6c603 100644 --- a/packages/util/src/bytes.ts +++ b/packages/util/src/bytes.ts @@ -56,8 +56,9 @@ for (let i = 0; i <= 256 * 256 - 1; i++) { * @returns {bigint} */ export const bytesToBigInt = (bytes: Uint8Array, littleEndian = false): bigint => { + assertIsBytes(bytes) if (littleEndian) { - bytes.reverse() + bytes = bytes.slice().reverse() } const hex = bytesToHex(bytes) if (hex === '0x') { @@ -122,48 +123,72 @@ export const bigIntToBytes = (num: bigint, littleEndian = false): Uint8Array => /** * Pads a `Uint8Array` with zeros till it has `length` bytes. - * Truncates the beginning or end of input if its length exceeds `length`. + * Throws if input length exceeds target length, unless allowTruncate is true. * @param {Uint8Array} msg the value to pad * @param {number} length the number of bytes the output should be - * @param {boolean} right whether to start padding form the left or right + * @param {boolean} right whether to start padding from the left or right + * @param {boolean} allowTruncate whether to allow truncation if msg exceeds length * @return {Uint8Array} */ -const setLength = (msg: Uint8Array, length: number, right: boolean): Uint8Array => { - if (right) { - if (msg.length < length) { - return new Uint8Array([...msg, ...new Uint8Array(length - msg.length)]) - } - return msg.subarray(0, length) - } else { - if (msg.length < length) { - return new Uint8Array([...new Uint8Array(length - msg.length), ...msg]) +const setLength = ( + msg: Uint8Array, + length: number, + right: boolean, + allowTruncate: boolean, +): Uint8Array => { + if (msg.length > length) { + if (!allowTruncate) { + throw EthereumJSErrorWithoutCode( + `Input length ${msg.length} exceeds target length ${length}. Use allowTruncate option to truncate.`, + ) } - return msg.subarray(-length) + return right ? msg.subarray(0, length) : msg.subarray(-length) } + if (msg.length < length) { + return right + ? new Uint8Array([...msg, ...new Uint8Array(length - msg.length)]) + : new Uint8Array([...new Uint8Array(length - msg.length), ...msg]) + } + return msg +} + +export interface SetLengthOpts { + /** Allow truncation if msg exceeds length. Default: false */ + allowTruncate?: boolean } /** * Left Pads a `Uint8Array` with leading zeros till it has `length` bytes. - * Or it truncates the beginning if it exceeds. + * Throws if input length exceeds target length, unless allowTruncate option is true. * @param {Uint8Array} msg the value to pad * @param {number} length the number of bytes the output should be + * @param {SetLengthOpts} opts options object with allowTruncate flag * @return {Uint8Array} */ -export const setLengthLeft = (msg: Uint8Array, length: number): Uint8Array => { +export const setLengthLeft = ( + msg: Uint8Array, + length: number, + opts: SetLengthOpts = {}, +): Uint8Array => { assertIsBytes(msg) - return setLength(msg, length, false) + return setLength(msg, length, false, opts.allowTruncate ?? false) } /** * Right Pads a `Uint8Array` with trailing zeros till it has `length` bytes. - * it truncates the end if it exceeds. + * Throws if input length exceeds target length, unless allowTruncate option is true. * @param {Uint8Array} msg the value to pad * @param {number} length the number of bytes the output should be + * @param {SetLengthOpts} opts options object with allowTruncate flag * @return {Uint8Array} */ -export const setLengthRight = (msg: Uint8Array, length: number): Uint8Array => { +export const setLengthRight = ( + msg: Uint8Array, + length: number, + opts: SetLengthOpts = {}, +): Uint8Array => { assertIsBytes(msg) - return setLength(msg, length, true) + return setLength(msg, length, true, opts.allowTruncate ?? false) } /** @@ -380,8 +405,8 @@ export const bigIntToAddressBytes = (value: bigint, strict: boolean = true): Uin throw Error(`Invalid address bytes length=${addressBytes.length} strict=${strict}`) } - // setLength already slices if more than requisite length - return setLengthLeft(addressBytes, 20) + // When not strict, allow truncation of values larger than 20 bytes + return setLengthLeft(addressBytes, 20, { allowTruncate: !strict }) } /** @@ -447,7 +472,7 @@ export const concatBytes = (...arrays: Uint8Array[]): Uint8Array => */ export function bytesToInt32(bytes: Uint8Array, littleEndian: boolean = false): number { if (bytes.length < 4) { - bytes = setLength(bytes, 4, littleEndian) + bytes = setLength(bytes, 4, littleEndian, false) } const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) return dataView.getUint32(0, littleEndian) @@ -461,7 +486,7 @@ export function bytesToInt32(bytes: Uint8Array, littleEndian: boolean = false): */ export function bytesToBigInt64(bytes: Uint8Array, littleEndian: boolean = false): bigint { if (bytes.length < 8) { - bytes = setLength(bytes, 8, littleEndian) + bytes = setLength(bytes, 8, littleEndian, false) } const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) return dataView.getBigUint64(0, littleEndian) diff --git a/packages/util/test/bytes.spec.ts b/packages/util/test/bytes.spec.ts index 45e2fb7735..110a88fb89 100644 --- a/packages/util/test/bytes.spec.ts +++ b/packages/util/test/bytes.spec.ts @@ -102,9 +102,15 @@ describe('setLengthLeft', () => { const padded = setLengthLeft(bytes, 3) assert.strictEqual(bytesToHex(padded), '0x000909') }) - it('should left truncate a Uint8Array', () => { + it('should throw by default when input exceeds target length', () => { const bytes = new Uint8Array([9, 0, 9]) - const padded = setLengthLeft(bytes, 2) + assert.throws(function () { + setLengthLeft(bytes, 2) + }, /Input length 3 exceeds target length 2/) + }) + it('should left truncate a Uint8Array when allowTruncate is true', () => { + const bytes = new Uint8Array([9, 0, 9]) + const padded = setLengthLeft(bytes, 2, { allowTruncate: true }) assert.strictEqual(bytesToHex(padded), '0x0009') }) it('should throw if input is not a Uint8Array', () => { @@ -121,9 +127,15 @@ describe('setLengthRight', () => { const padded = setLengthRight(bytes, 3) assert.strictEqual(bytesToHex(padded), '0x090900') }) - it('should right truncate a Uint8Array', () => { + it('should throw by default when input exceeds target length', () => { + const bytes = new Uint8Array([9, 0, 9]) + assert.throws(function () { + setLengthRight(bytes, 2) + }, /Input length 3 exceeds target length 2/) + }) + it('should right truncate a Uint8Array when allowTruncate is true', () => { const bytes = new Uint8Array([9, 0, 9]) - const padded = setLengthRight(bytes, 2) + const padded = setLengthRight(bytes, 2, { allowTruncate: true }) assert.strictEqual(bytesToHex(padded), '0x0900') }) it('should throw if input is not a Uint8Array', () => { @@ -372,6 +384,26 @@ describe('bytesToBigInt', () => { const buf = hexToBytes('0x123') assert.strictEqual(BigInt(0x123), bytesToBigInt(buf)) }) + it('should return 0n for empty Uint8Array', () => { + assert.strictEqual(bytesToBigInt(new Uint8Array(0)), 0n) + }) + it('should throw if input is not a Uint8Array', () => { + assert.throws(function () { + // @ts-expect-error -- Testing invalid input + bytesToBigInt([1, 2, 3]) + }, /This method only supports Uint8Array/) + }) + it('should not mutate input when littleEndian is true', () => { + const bytes = new Uint8Array([0x01, 0x02, 0x03]) + const original = new Uint8Array([0x01, 0x02, 0x03]) + bytesToBigInt(bytes, true) + assert.deepEqual(bytes, original) + }) + it('should correctly convert littleEndian bytes', () => { + // 0x030201 in big-endian = 0x010203 in little-endian + const bytes = new Uint8Array([0x01, 0x02, 0x03]) + assert.strictEqual(bytesToBigInt(bytes, true), BigInt(0x030201)) + }) }) describe('bigIntToBytes', () => {