Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/evm/src/precompiles/01-ecrecover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion packages/tx/src/capabilities/eip7702.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
MAX_UINT64,
bytesToBigInt,
validateNoLeadingZeroes,
validateNoLeadingZeroesAllowZero,
} from '@ethereumjs/util'
import type { EIP7702CompatibleTx } from '../types.ts'

Expand Down Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions packages/tx/test/eip7702.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
4 changes: 2 additions & 2 deletions packages/util/src/binaryTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
99 changes: 76 additions & 23 deletions packages/util/src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 })
}

/**
Expand Down Expand Up @@ -447,7 +500,7 @@ export const concatBytes = (...arrays: Uint8Array[]): Uint8Array<ArrayBuffer> =>
*/
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)
Expand All @@ -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)
Expand Down
40 changes: 36 additions & 4 deletions packages/util/test/bytes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/vm/test/api/EIPs/eip-3860.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ describe('EIP 3860 tests', () => {
'initcode exceeds max size',
)
})
})
}, 10000)
Loading