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/tx/src/capabilities/eip7702.ts b/packages/tx/src/capabilities/eip7702.ts index f37b79c545..3182e26037 100644 --- a/packages/tx/src/capabilities/eip7702.ts +++ b/packages/tx/src/capabilities/eip7702.ts @@ -6,6 +6,7 @@ import { MAX_UINT64, bytesToBigInt, validateNoLeadingZeroes, + validateNoLeadingZeroesAllowZero, } from '@ethereumjs/util' import type { EIP7702CompatibleTx } from '../types.ts' @@ -49,7 +50,10 @@ export function verifyAuthorizationList(tx: EIP7702CompatibleTx) { const [chainId, address, nonce, yParity, r, s] = item - validateNoLeadingZeroes({ yParity, r, s, nonce, chainId }) + // For chainId and yParity, we allow 0x00 (single zero byte) as valid encoding for zero + // See: https://github.com/ethereumjs/ethereumjs-monorepo/issues/4057 + validateNoLeadingZeroesAllowZero({ chainId, yParity }) + validateNoLeadingZeroes({ r, s, nonce }) if (address.length !== 20) { throw EthereumJSErrorWithoutCode( diff --git a/packages/tx/test/eip7702.spec.ts b/packages/tx/test/eip7702.spec.ts index 1c22bb0213..63f37df3d9 100644 --- a/packages/tx/test/eip7702.spec.ts +++ b/packages/tx/test/eip7702.spec.ts @@ -118,5 +118,15 @@ describe('[EOACode7702Transaction]', () => { assert.doesNotThrow(() => { createEOACode7702Tx(getTxData(), { common }) }) + + // 0x00 (single zero byte) should be allowed for chainId and yParity + // See: https://github.com/ethereumjs/ethereumjs-monorepo/issues/4057 + assert.doesNotThrow(() => { + createEOACode7702Tx(getTxData({ chainId: '0x00' }), { common }) + }, 'chainId 0x00 should be allowed') + + assert.doesNotThrow(() => { + createEOACode7702Tx(getTxData({ yParity: '0x00' }), { common }) + }, 'yParity 0x00 should be allowed') }) }) 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..49b3ff31b9 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) } /** @@ -343,6 +368,34 @@ export const validateNoLeadingZeroes = (values: { [key: string]: Uint8Array | un } } +/** + * Checks provided Uint8Array for leading zeroes and throws if found. + * This variant allows a single zero byte (0x00) which represents the value 0. + * + * Examples: + * + * Valid values: 0x1, 0x, 0x00, 0x01, 0x1234 + * Invalid values: 0x001, 0x0001 + * + * Note: This method is useful for validating integers that should have no leading zero bytes, + * but where a single 0x00 byte is acceptable to represent the value zero (e.g., in EIP-7702 + * authorization lists where chainId=0 and yParity=0 are valid values). + * @param values An object containing string keys and Uint8Array values + * @throws if any provided value is found to have leading zero bytes (except single 0x00) + */ +export const validateNoLeadingZeroesAllowZero = (values: { + [key: string]: Uint8Array | undefined +}) => { + for (const [k, v] of Object.entries(values)) { + // Allow empty array [] and single zero byte [0], but reject [0, ...] with length > 1 + if (v !== undefined && v.length > 1 && v[0] === 0) { + throw EthereumJSErrorWithoutCode( + `${k} cannot have leading zeroes, received: ${bytesToHex(v)}`, + ) + } + } +} + /** * Converts a {@link bigint} to a `0x` prefixed hex string * @param {bigint} num the bigint to convert @@ -380,8 +433,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 +500,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 +514,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', () => { diff --git a/packages/vm/test/api/EIPs/eip-3860.spec.ts b/packages/vm/test/api/EIPs/eip-3860.spec.ts index 299f02fbc6..c649442916 100644 --- a/packages/vm/test/api/EIPs/eip-3860.spec.ts +++ b/packages/vm/test/api/EIPs/eip-3860.spec.ts @@ -45,4 +45,4 @@ describe('EIP 3860 tests', () => { 'initcode exceeds max size', ) }) -}) +}, 10000)