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
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
71 changes: 48 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 @@ -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 })
}

/**
Expand Down Expand Up @@ -447,7 +472,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 +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)
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
Loading