diff --git a/package-lock.json b/package-lock.json index 2d3b307f24..244b730d2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5118,6 +5118,14 @@ "node": ">=14.0.0" } }, + "node_modules/bigint-mod-arith": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.3.1.tgz", + "integrity": "sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w==", + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", @@ -17618,6 +17626,7 @@ "@ethereumjs/statemanager": "^3.0.0-alpha.1", "@ethereumjs/util": "^10.0.0-alpha.1", "@types/debug": "^4.1.9", + "bigint-mod-arith": "^3.3.1", "debug": "^4.3.3", "ethereum-cryptography": "^3.0.0", "eventemitter3": "^5.0.1" diff --git a/packages/common/src/chains.ts b/packages/common/src/chains.ts index eb1cb3b21f..993af6b610 100644 --- a/packages/common/src/chains.ts +++ b/packages/common/src/chains.ts @@ -121,6 +121,10 @@ export const Mainnet: ChainConfig = { name: 'osaka', block: null, }, + { + name: 'evmmax', + block: null, + }, ], bootstrapNodes: [ { diff --git a/packages/common/src/enums.ts b/packages/common/src/enums.ts index 8f4f673d57..708dd1c11c 100644 --- a/packages/common/src/enums.ts +++ b/packages/common/src/enums.ts @@ -73,6 +73,7 @@ export enum Hardfork { Prague = 'prague', Osaka = 'osaka', Verkle = 'verkle', + EVMMax = 'evmmax', } export enum ConsensusType { diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index 4f84adde69..94263717eb 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -171,4 +171,7 @@ export const hardforksDict: HardforksDict = { verkle: { eips: [2935, 4762, 6800], }, + evmmax: { + eips: [6690], + }, } diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 551847425c..c140abc7d8 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -149,6 +149,7 @@ function parseGethParams(json: any) { [Hardfork.Cancun]: { name: 'cancunTime', postMerge: true, isTimestamp: true }, [Hardfork.Prague]: { name: 'pragueTime', postMerge: true, isTimestamp: true }, [Hardfork.Verkle]: { name: 'verkleTime', postMerge: true, isTimestamp: true }, + [Hardfork.EVMMax]: { name: 'evmmaxTime', postMerge: true, isTimestamp: true }, } // forkMapRev is the map from config field name to Hardfork diff --git a/packages/evm/package.json b/packages/evm/package.json index e4ee11dbae..ee9abf0421 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -49,6 +49,7 @@ "@ethereumjs/statemanager": "^3.0.0-alpha.1", "@ethereumjs/util": "^10.0.0-alpha.1", "@types/debug": "^4.1.9", + "bigint-mod-arith": "^3.3.1", "debug": "^4.3.3", "ethereum-cryptography": "^3.0.0", "eventemitter3": "^5.0.1" diff --git a/packages/evm/src/evmmax/arith.ts b/packages/evm/src/evmmax/arith.ts new file mode 100644 index 0000000000..72939f4d5b --- /dev/null +++ b/packages/evm/src/evmmax/arith.ts @@ -0,0 +1,174 @@ +const MASK_64 = (1n << 64n) - 1n + +export function putUint64BE(dst: Uint8Array, offset: number, value: bigint): void { + value = BigInt.asUintN(64, value) + const hex = value.toString(16).padStart(16, '0') + for (let i = 0; i < 8; i++) { + dst[offset + i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16) + } +} + +export function negModInverse(mod: bigint): bigint { + let k0 = (2n - mod) & MASK_64 + let t = (mod - 1n) & MASK_64 + + for (let i = 1; i < 64; i <<= 1) { + t = (t * t) & MASK_64 + k0 = (k0 * ((t + 1n) & MASK_64)) & MASK_64 + } + k0 = -k0 & MASK_64 + + return k0 +} + +export function bytesToLimbs(b: Uint8Array): bigint[] { + const wordCount = Math.ceil(b.length / 8) + const paddedSize = wordCount * 8 + + const paddedBytes = new Uint8Array(paddedSize) + paddedBytes.set(b, paddedSize - b.length) + + const limbs: bigint[] = new Array(wordCount) + + // Extract each 64-bit word in big-endian order + for (let i = 0; i < wordCount; i++) { + const offset = i * 8 + // Construct the 64-bit limb as a bigint + const limb = + (BigInt(paddedBytes[offset]) << 56n) | + (BigInt(paddedBytes[offset + 1]) << 48n) | + (BigInt(paddedBytes[offset + 2]) << 40n) | + (BigInt(paddedBytes[offset + 3]) << 32n) | + (BigInt(paddedBytes[offset + 4]) << 24n) | + (BigInt(paddedBytes[offset + 5]) << 16n) | + (BigInt(paddedBytes[offset + 6]) << 8n) | + BigInt(paddedBytes[offset + 7]) + limbs[i] = limb + } + + // Reverse the limbs to get little-endian + limbs.reverse() + + return limbs +} + +function limbsToBytes(limbs: bigint[]): Uint8Array { + const limbCount = limbs.length + const result = new Uint8Array(limbCount * 8) + + for (let i = 0; i < limbCount; i++) { + const limb = limbs[limbCount - 1 - i] + // Extract 8 bytes in big-endian order + const offset = i * 8 + result[offset] = Number((limb >> 56n) & 0xffn) + result[offset + 1] = Number((limb >> 48n) & 0xffn) + result[offset + 2] = Number((limb >> 40n) & 0xffn) + result[offset + 3] = Number((limb >> 32n) & 0xffn) + result[offset + 4] = Number((limb >> 24n) & 0xffn) + result[offset + 5] = Number((limb >> 16n) & 0xffn) + result[offset + 6] = Number((limb >> 8n) & 0xffn) + result[offset + 7] = Number(limb & 0xffn) + } + + // Remove leading zeros: + let firstNonZero = 0 + while (firstNonZero < result.length && result[firstNonZero] === 0) { + firstNonZero++ + } + + return firstNonZero === result.length ? new Uint8Array([0]) : result.slice(firstNonZero) +} + +function limbsToInt(limbs: bigint[]): bigint { + const numBytes = limbsToBytes(limbs) + return uint8ArrayToBigint(numBytes) +} + +// Helper function to convert a Uint8Array (big-endian) to bigint +function uint8ArrayToBigint(arr: Uint8Array): bigint { + if (arr.length === 0) return 0n + const hex = '0x' + Array.from(arr, (byte) => byte.toString(16).padStart(2, '0')).join('') + return BigInt(hex) +} + +function placeBEBytesInOutput(out: bigint[], b: Uint8Array): void { + const padded = new Uint8Array(out.length * 8) + padded.set(b, padded.length - b.length) + + const resultLimbs = out.length + for (let i = 0; i < resultLimbs; i++) { + const offset = i * 8 + let limb = 0n + limb |= BigInt(padded[offset]) << 56n + limb |= BigInt(padded[offset + 1]) << 48n + limb |= BigInt(padded[offset + 2]) << 40n + limb |= BigInt(padded[offset + 3]) << 32n + limb |= BigInt(padded[offset + 4]) << 24n + limb |= BigInt(padded[offset + 5]) << 16n + limb |= BigInt(padded[offset + 6]) << 8n + limb |= BigInt(padded[offset + 7]) + + out[resultLimbs - 1 - i] = limb + } +} + +function intToBEBytes(value: bigint): Uint8Array { + if (value === 0n) return new Uint8Array([0]) + let hex = value.toString(16) + if (hex.length % 2 !== 0) { + hex = '0' + hex + } + const arr = new Uint8Array(hex.length / 2) + for (let i = 0; i < arr.length; i++) { + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16) + } + return arr +} + +export function mulModBinary( + z: bigint[], + x: bigint[], + y: bigint[], + modulus: bigint[], + modInv: bigint, +) { + const X = limbsToInt(x) + const Y = limbsToInt(y) + const M = limbsToInt(modulus) + + const result = (X * Y) % M + const resultBytes = intToBEBytes(result) + placeBEBytesInOutput(z, resultBytes) +} + +export function addModBinary(z: bigint[], x: bigint[], y: bigint[], modulus: bigint[]) { + const X = limbsToInt(x) + const Y = limbsToInt(y) + const M = limbsToInt(modulus) + + const result = (X + Y) % M + const resultBytes = intToBEBytes(result) + placeBEBytesInOutput(z, resultBytes) +} + +export function subModBinary(z: bigint[], x: bigint[], y: bigint[], modulus: bigint[]) { + const X = limbsToInt(x) + const Y = limbsToInt(y) + const M = limbsToInt(modulus) + + let result = (X - Y) % M + if (result < 0n) { + result += M + } + const resultBytes = intToBEBytes(result) + placeBEBytesInOutput(z, resultBytes) +} + +export function lt(x: bigint[], y: bigint[]): boolean { + for (let i = x.length; i > 0; i--) { + if (x[i - 1] < y[i - 1]) { + return true + } + } + return false +} diff --git a/packages/evm/src/evmmax/fieldContext.ts b/packages/evm/src/evmmax/fieldContext.ts new file mode 100644 index 0000000000..13bf8ca305 --- /dev/null +++ b/packages/evm/src/evmmax/fieldContext.ts @@ -0,0 +1,309 @@ +import { BIGINT_8, bigIntToBytes, bytesToBigInt, concatBytes } from '@ethereumjs/util' + +import { + addModBinary, + bytesToLimbs, + lt, + mulModBinary, + negModInverse, + putUint64BE, + subModBinary, +} from './arith.js' + +const MAX_MODULUS_SIZE = 96 // 768 bit max modulus width +const ZERO_BYTE = Uint8Array.from([0]) + +export function isModulusBinary(modulus: bigint): boolean { + return modulus > 0n && (modulus & (modulus - 1n)) === 0n +} + +export class FieldContext { + public modulus: bigint[] + public r2: bigint[] | undefined + public modInvVal: bigint | undefined + + public useMontgomeryRepr: boolean + public isModulusBinary: boolean + + public scratchSpace: bigint[] + public addSubCost: bigint | undefined + public mulCost: bigint | undefined + + public addMod: Function + public subMod: Function + public mulMod: Function + + public one: bigint[] | undefined + public modulusInt: bigint + public elemSize: bigint + public scratchSpaceElemCount: bigint + public outputWriteBuf: bigint[] | undefined + + constructor(modBytes: Uint8Array, scratchSize: bigint) { + if (modBytes.length > MAX_MODULUS_SIZE) { + throw new Error('modulus cannot be greater than 768 bits') + } + if (modBytes.length === 0) { + throw new Error('modulus must be non-empty') + } + if (modBytes.subarray(0, 2) === ZERO_BYTE) { + throw new Error('most significant byte of modulus must not be zero') + } + if (scratchSize === 0n) { + throw new Error('scratch space must have non-zero size') + } + if (scratchSize > 256n) { + throw new Error('scratch space can allocate a maximum of 256 field elements') + } + + const mod = bytesToBigInt(modBytes) + const paddedSize = BigInt(Math.ceil(modBytes.length / 8) * 8) // Compute paddedSize as the next multiple of 8 bytes + + // console.log('dbg110') + // console.log(scratchSize) + // console.log(paddedSize) + // console.log((paddedSize / BIGINT_8) * scratchSize) + + if (isModulusBinary(mod)) { + this.modulus = bytesToLimbs(modBytes) + this.mulMod = mulModBinary + this.addMod = addModBinary + this.subMod = subModBinary + this.scratchSpace = new Array(Number((paddedSize / BIGINT_8) * scratchSize)).fill(0n) + this.outputWriteBuf = new Array(this.scratchSpace.length).fill(0n) + this.scratchSpaceElemCount = BigInt(scratchSize) + this.modulusInt = mod + this.elemSize = paddedSize / 8n + this.useMontgomeryRepr = false + this.isModulusBinary = true + + // console.log(this.scratchSpace) + + return + } + + if (modBytes.at(-1)! % 2 === 0) { + throw new Error('modulus cannot be even') + } + + const negModInv = negModInverse(mod) + + const paddedSizeBig = BigInt(paddedSize) + const shiftAmount = paddedSizeBig * 16n + const r2 = (1n << shiftAmount) % mod + + let r2Bytes = bigIntToBytes(r2) + if (modBytes.length < paddedSize) { + modBytes = concatBytes(new Uint8Array(Number(paddedSize) - modBytes.length), modBytes) + } + if (r2Bytes.length < paddedSize) { + r2Bytes = concatBytes(new Uint8Array(Number(paddedSize) - r2Bytes.length), r2Bytes) + } + const one = new Array(paddedSize / BIGINT_8).fill(0n) + one[0] = 1n + + this.modulus = bytesToLimbs(modBytes) + this.modInvVal = negModInv + this.r2 = bytesToLimbs(r2Bytes) + this.mulMod = () => {} // TODO filler function, replace with actual + this.addMod = () => {} // TODO filler function, replace with actual + this.subMod = () => {} // TODO filler function, replace with actual + this.scratchSpace = new Array((paddedSize / BIGINT_8) * scratchSize).fill(0n) + this.scratchSpaceElemCount = BigInt(scratchSize) + this.one = one + this.modulusInt = mod + this.elemSize = paddedSize + this.useMontgomeryRepr = true + this.isModulusBinary = false + } + + store(dst: number, count: number, from: Uint8Array) { + const elemSize = this.modulus.length + + for (let i = 0; i < count; i++) { + const srcIdx = i * elemSize * 8 + const dstIdx = dst * elemSize + i * elemSize + + const val = bytesToLimbs(from.slice(srcIdx, srcIdx + elemSize * 8)) + if (!lt(val, this.modulus)) throw new Error(`value being stored must be less than modulus`) + + if (this.useMontgomeryRepr) { + // TODO + } else { + for (let i = 0; i < elemSize; i++) { + this.scratchSpace[dstIdx + i] = val[i] + } + } + } + } + + /** + * Load 'count' field elements from this.scratchSpace (starting at index 'from') + * into the provided 'dst' Uint8Array. + */ + public Load(dst: Uint8Array, from: number, count: number): void { + const elemSize = this.modulus.length + let dstIdx = 0 + + for (let srcIdx = from; srcIdx < from + count; srcIdx++) { + // temp array to hold limbs + const res = new Array(elemSize) + + if (this.useMontgomeryRepr) { + // TODO + } else { + // Directly copy from scratchSpace + const slice = this.scratchSpace.slice(srcIdx * elemSize, (srcIdx + 1) * elemSize) + for (let i = 0; i < elemSize; i++) { + res[i] = slice[i] + } + } + + // Write res[] into 'dst' + for (let i = 0; i < elemSize; i++) { + const limb = res[elemSize - 1 - i] + putUint64BE(dst, dstIdx + i * 8, limb) + } + dstIdx += elemSize * 8 + } + } + + /** + * MulMod computes 'count' modular multiplications, pairwise multiplying values + * from offsets [x, x+xStride, x+xStride*2, ..., x+xStride*(count - 1)] + * and [y, y+yStride, y+yStride*2, ..., y+yStride*(count - 1)] + * placing the result in [out, out+outStride, out+outStride*2, ..., out+outStride*(count - 1)]. + */ + public mulM( + outIndex: number, + outStride: number, + x: number, + xStride: number, + y: number, + yStride: number, + count: number, + ): void { + const elemSize = this.modulus.length + + // perform the multiplications, writing into outputWriteBuf + for (let i = 0; i < count; i++) { + const xSrc = (x + i * xStride) * elemSize + const ySrc = (y + i * yStride) * elemSize + const dst = (outIndex + i * outStride) * elemSize + + const xSlice = this.scratchSpace.slice(xSrc, xSrc + elemSize) + const ySlice = this.scratchSpace.slice(ySrc, ySrc + elemSize) + + const outSlice = this.outputWriteBuf!.slice(dst, dst + elemSize) + + this.mulMod(outSlice, xSlice, ySlice, this.modulus, this.modInvVal) + + for (let j = 0; j < elemSize; j++) { + this.outputWriteBuf![dst + j] = outSlice[j] + } + } + + // copy the result from outputWriteBuf into scratchSpace + for (let i = 0; i < count; i++) { + const offset = (outIndex + i * outStride) * elemSize + for (let j = 0; j < elemSize; j++) { + this.scratchSpace[offset + j] = this.outputWriteBuf![offset + j] + } + } + } + + /** + * SubMod computes 'count' modular subtractions, pairwise subtracting values + * at offsets [x, x+xStride, ..., x+xStride*(count - 1)] and + * [y, y+yStride, ..., y+yStride*(count - 1)] + * placing the result in [out, out+outStride, ...]. + */ + public subM( + outIndex: number, + outStride: number, + x: number, + xStride: number, + y: number, + yStride: number, + count: number, + ): void { + const elemSize = this.modulus.length + + // perform the subtractions into outputWriteBuf + for (let i = 0; i < count; i++) { + const xSrc = (x + i * xStride) * elemSize + const ySrc = (y + i * yStride) * elemSize + const dst = (outIndex + i * outStride) * elemSize + + const xSlice = this.scratchSpace.slice(xSrc, xSrc + elemSize) + const ySlice = this.scratchSpace.slice(ySrc, ySrc + elemSize) + const outSlice = this.outputWriteBuf!.slice(dst, dst + elemSize) + + this.subMod(outSlice, xSlice, ySlice, this.modulus) + + for (let j = 0; j < elemSize; j++) { + this.outputWriteBuf![dst + j] = outSlice[j] + } + } + + // copy from outputWriteBuf into scratchSpace + for (let i = 0; i < count; i++) { + const offset = (outIndex + i * outStride) * elemSize + for (let j = 0; j < elemSize; j++) { + this.scratchSpace[offset + j] = this.outputWriteBuf![offset + j] + } + } + } + + /** + * AddMod computes 'count' modular additions, pairwise adding values + * at offsets [x, x+xStride, ..., x+xStride*(count - 1)] and + * [y, y+yStride, ..., y+yStride*(count - 1)] + * placing the result in [out, out+outStride, ...]. + */ + public addM( + outIndex: number, + outStride: number, + x: number, + xStride: number, + y: number, + yStride: number, + count: number, + ): void { + const elemSize = Number(this.elemSize) + + // perform the additions, writing to outputWriteBuf + for (let i = 0; i < count; i++) { + const xSrc = (x + i * xStride) * elemSize + const ySrc = (y + i * yStride) * elemSize + const dst = (outIndex + i * outStride) * elemSize + + const xSlice = this.scratchSpace.slice(xSrc, xSrc + elemSize) + const ySlice = this.scratchSpace.slice(ySrc, ySrc + elemSize) + const outSlice = this.outputWriteBuf!.slice(dst, dst + elemSize) + + // console.log('dbg100') + // console.log(this.outputWriteBuf) + // console.log(elemSize) + // console.log(xSrc) + // console.log(ySrc) + // console.log(xSlice) + // console.log(ySlice) + // console.log(outSlice) + + this.addMod(outSlice, xSlice, ySlice, this.modulus) + + for (let j = 0; j < elemSize; j++) { + this.outputWriteBuf![dst + j] = outSlice[j] + } + } + + // copy from outputWriteBuf into scratchSpace + for (let i = 0; i < count; i++) { + const offset = (outIndex + i * outStride) * elemSize + for (let j = 0; j < elemSize; j++) { + this.scratchSpace[offset + j] = this.outputWriteBuf![offset + j] + } + } + } +} diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts index 17b280c15f..a37510b6ac 100644 --- a/packages/evm/src/index.ts +++ b/packages/evm/src/index.ts @@ -60,5 +60,6 @@ export { } export * from './constructors.js' +export * from './evmmax/fieldContext.js' export * from './params.js' export * from './verkleAccessWitness.js' diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index 9ba11f900b..ee348d448f 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -16,6 +16,7 @@ import { FORMAT, MAGIC, VERSION } from './eof/constants.js' import { EOFContainerMode, validateEOF } from './eof/container.js' import { setupEOF } from './eof/setup.js' import { ContainerSectionType } from './eof/verify.js' +import { FieldContext } from './evmmax/fieldContext.js' import { ERROR, EvmError } from './exceptions.js' import { type EVMPerformanceLogger, type Timer } from './logger.js' import { Memory } from './memory.js' @@ -105,6 +106,7 @@ export interface RunState { gasRefund: bigint // Tracks the current refund gasLeft: bigint // Current gas left returnBytes: Uint8Array /* Current bytes in the return Uint8Array. Cleared each time a CALL/CREATE is made in the current frame. */ + evmmaxState: FieldContext } export interface InterpreterResult { @@ -190,6 +192,7 @@ export class Interpreter { stateManager: this._stateManager, blockchain, env, + evmmaxState: new FieldContext(new Uint8Array(), 0n), shouldDoJumpAnalysis: true, interpreter: this, gasRefund: env.gasRefund, diff --git a/packages/evm/src/opcodes/codes.ts b/packages/evm/src/opcodes/codes.ts index 42993e7853..2e36abba17 100644 --- a/packages/evm/src/opcodes/codes.ts +++ b/packages/evm/src/opcodes/codes.ts @@ -210,6 +210,14 @@ const opcodes: OpcodeEntry = { 0xa3: dynamicGasOp('LOG'), 0xa4: dynamicGasOp('LOG'), + // '0xf0' range - extended range/width modular arithmetic + 0xc0: asyncAndDynamicGasOp('SETMODX'), + 0xc1: asyncAndDynamicGasOp('LOADX'), + 0xc2: asyncAndDynamicGasOp('STOREX'), + 0xc3: asyncAndDynamicGasOp('ADDMODX'), + 0xc4: asyncAndDynamicGasOp('SUBMODX'), + 0xc5: asyncAndDynamicGasOp('MULMODX'), + // '0xf0' range - closures 0xf0: asyncAndDynamicGasOp('CREATE'), 0xf1: asyncAndDynamicGasOp('CALL'), @@ -372,6 +380,19 @@ const eipOpcodes: { eip: number; opcodes: OpcodeEntry }[] = [ 0xee: asyncAndDynamicGasOp('RETURNCONTRACT'), }, }, + { + eip: 6690, + opcodes: { + // control & i/o + 0xc0: asyncAndDynamicGasOp('SETMODX'), + 0xc1: asyncAndDynamicGasOp('LOADX'), + 0xc2: asyncAndDynamicGasOp('STOREX'), + // arithmetic + 0xc3: asyncAndDynamicGasOp('ADDMODX'), + 0xc4: asyncAndDynamicGasOp('SUBMODX'), + 0xc5: asyncAndDynamicGasOp('MULMODX'), + }, + }, ] /** diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index f7552608b5..4ced0e1f71 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -1016,6 +1016,48 @@ export const handlers: Map = new Map([ runState.interpreter.log(mem, topicsCount, topicsBuf) }, ], + // 0xc0: SETMODX + [ + 0xc0, + function (runState, _common) { + return + }, + ], + // 0xc1: STOREX + [ + 0xc1, + function (runState, _common) { + return + }, + ], + // 0xc2: LOADX + [ + 0xc2, + function (runState, _common) { + return + }, + ], + // 0xc3: ADDMODX + [ + 0xc3, + function (runState, _common) { + return + }, + ], + // 0xc4: SUBMODX + [ + 0xc4, + function (runState, _common) { + return + }, + ], + // 0xc5: MULMODX + [ + 0xc5, + function (runState, _common) { + return + }, + ], // 0xd0: DATALOAD [ 0xd0, diff --git a/packages/evm/test/evmmax/fieldContext.spec.ts b/packages/evm/test/evmmax/fieldContext.spec.ts new file mode 100644 index 0000000000..e4f45a965a --- /dev/null +++ b/packages/evm/test/evmmax/fieldContext.spec.ts @@ -0,0 +1,73 @@ +import { bigIntToBytes, bytesToBigInt } from '@ethereumjs/util' +import { randomBytes } from 'crypto' +import { assert, describe, it } from 'vitest' + +import { FieldContext } from '../../src/index.js' + +function randomBigInt(size: number, limit: bigint): bigint { + return bytesToBigInt(randomBytes(size)) % limit +} + +function randomBinaryModulus(size: number): bigint { + return 1n << BigInt(size * 8) +} + +export function randomOddBigInt(size: number, limit: bigint): bigint { + let num + let bytes + while (true) { + num = randomBigInt(size, limit) + bytes = bigIntToBytes(num) + if (bytes[bytes.length - 1] % 2 !== 0) return num + } +} + +function padBigIntBytes(val: bigint, byteLen: number): Uint8Array { + const raw = bigIntToBytes(val) + if (raw.length === byteLen) return raw + const out = new Uint8Array(byteLen) + out.set(raw, byteLen - raw.length) + return out +} + +describe('FieldContext modular arithmetic', () => { + for (let i = 1; i < 96; i++) { + it(`should do add, sub, mul under a random modulus of size ${i} bytes`, () => { + const mod = randomBinaryModulus(i) + const modBytes = bigIntToBytes(mod) + const fieldCtx = new FieldContext(modBytes, 256n) + + const xInt = randomBigInt(modBytes.length, mod) + const yInt = randomBigInt(modBytes.length, mod) + + // convert operands to padded bytes for storing + const elemByteLen = Number(fieldCtx.elemSize) + const xBytes = padBigIntBytes(xInt, elemByteLen * 8) + const yBytes = padBigIntBytes(yInt, elemByteLen * 8) + + fieldCtx.store(1, 1, xBytes) + fieldCtx.store(2, 1, yBytes) + + const outBytes = new Uint8Array(elemByteLen * 8) + + fieldCtx.addM(0, 1, 1, 1, 2, 1, 1) + fieldCtx.Load(outBytes, 0, 1) + const expectedAdd = (xInt + yInt) % mod + const actualAdd = bytesToBigInt(outBytes) + assert.deepEqual(actualAdd, expectedAdd) + + fieldCtx.subM(0, 1, 1, 1, 2, 1, 1) + fieldCtx.Load(outBytes, 0, 1) + let expectedSub = (xInt - yInt) % mod + if (expectedSub < 0n) expectedSub += mod + const actualSub = bytesToBigInt(outBytes) + assert.deepEqual(actualSub, expectedSub) + + fieldCtx.mulM(0, 1, 1, 1, 2, 1, 1) + fieldCtx.Load(outBytes, 0, 1) + const expectedMul = (xInt * yInt) % mod + const actualMul = bytesToBigInt(outBytes) + assert.deepEqual(actualMul, expectedMul) + }) + } +})