diff --git a/package-lock.json b/package-lock.json index 2cc6aea911..f60e95c3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2159,8 +2159,7 @@ }, "node_modules/@noble/curves": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "resolved": "git+ssh://git@github.com/holgerd77/noble-curves.git#d4ccba71cfa549637d5c10322639d5467397c0fb", "dependencies": { "@noble/hashes": "1.4.0" }, @@ -16675,6 +16674,7 @@ "mcl-wasm": "^1.5.0", "memory-level": "^1.0.0", "prom-client": "^15.1.0", + "rustbn-wasm": "^0.4.0", "verkle-cryptography-wasm": "^0.4.5", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.5", @@ -16833,11 +16833,10 @@ "@ethereumjs/statemanager": "^2.3.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", - "@noble/curves": "^1.4.2", + "@noble/curves": "^1.5.0", "@types/debug": "^4.1.9", "debug": "^4.3.3", - "ethereum-cryptography": "^2.2.1", - "rustbn-wasm": "^0.4.0" + "ethereum-cryptography": "^2.2.1" }, "devDependencies": { "@ethersproject/abi": "^5.0.12", @@ -16854,6 +16853,7 @@ "minimist": "^1.2.5", "node-dir": "^0.1.17", "rollup-plugin-visualizer": "^5.12.0", + "rustbn-wasm": "^0.4.0", "solc": "^0.8.1", "split": "^1.0.1" }, @@ -16861,6 +16861,17 @@ "node": ">=18" } }, + "packages/evm/node_modules/@noble/curves": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz", + "integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/genesis": { "name": "@ethereumjs/genesis", "version": "0.2.2", diff --git a/packages/client/package.json b/packages/client/package.json index 9438ffa403..96a12eff64 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -88,6 +88,7 @@ "mcl-wasm": "^1.5.0", "memory-level": "^1.0.0", "prom-client": "^15.1.0", + "rustbn-wasm": "^0.4.0", "verkle-cryptography-wasm": "^0.4.5", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.5", diff --git a/packages/client/src/execution/vmexecution.ts b/packages/client/src/execution/vmexecution.ts index 8b45786f67..2d7a0090b1 100644 --- a/packages/client/src/execution/vmexecution.ts +++ b/packages/client/src/execution/vmexecution.ts @@ -5,7 +5,7 @@ import { DBSetTD, } from '@ethereumjs/blockchain' import { CacheType, ConsensusType, Hardfork } from '@ethereumjs/common' -import { MCLBLS } from '@ethereumjs/evm' +import { MCLBLS, RustBN254 } from '@ethereumjs/evm' import { getGenesis } from '@ethereumjs/genesis' import { DefaultStateManager, StatelessVerkleStateManager } from '@ethereumjs/statemanager' import { createTrie } from '@ethereumjs/trie' @@ -21,6 +21,7 @@ import { import { VM, runBlock, runTx } from '@ethereumjs/vm' import { writeFileSync } from 'fs' import * as mcl from 'mcl-wasm' +import { initRustBN } from 'rustbn-wasm' import { loadVerkleCrypto } from 'verkle-cryptography-wasm' import { Event } from '../types.js' @@ -178,12 +179,14 @@ export class VMExecution extends Execution { }) await mcl.init(mcl.BLS12_381) + const rustBN = await initRustBN() this.merkleVM = await VM.create({ common: this.config.execCommon, blockchain: this.chain.blockchain, stateManager, evmOpts: { bls: new MCLBLS(mcl), + bn254: new RustBN254(rustBN), }, profilerOpts: this.config.vmProfilerOpts, }) @@ -201,12 +204,14 @@ export class VMExecution extends Execution { verkleCrypto, }) await mcl.init(mcl.BLS12_381) + const rustBN = await initRustBN() this.verkleVM = await VM.create({ common: this.config.execCommon, blockchain: this.chain.blockchain, stateManager, evmOpts: { bls: new MCLBLS(mcl), + bn254: new RustBN254(rustBN), }, profilerOpts: this.config.vmProfilerOpts, }) diff --git a/packages/evm/package.json b/packages/evm/package.json index b136324d2d..a6cc3f5d97 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -60,11 +60,10 @@ "@ethereumjs/statemanager": "^2.3.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", - "@noble/curves": "^1.4.2", + "@noble/curves": "^1.5.0", "@types/debug": "^4.1.9", "debug": "^4.3.3", - "ethereum-cryptography": "^2.2.1", - "rustbn-wasm": "^0.4.0" + "ethereum-cryptography": "^2.2.1" }, "devDependencies": { "@ethersproject/abi": "^5.0.12", @@ -81,6 +80,7 @@ "minimist": "^1.2.5", "node-dir": "^0.1.17", "rollup-plugin-visualizer": "^5.12.0", + "rustbn-wasm": "^0.4.0", "solc": "^0.8.1", "split": "^1.0.1" }, diff --git a/packages/evm/src/constructors.ts b/packages/evm/src/constructors.ts index 16c6ecf607..381a542806 100644 --- a/packages/evm/src/constructors.ts +++ b/packages/evm/src/constructors.ts @@ -1,14 +1,12 @@ import { Common, Mainnet } from '@ethereumjs/common' import { SimpleStateManager } from '@ethereumjs/statemanager' -import { initRustBN } from 'rustbn-wasm' +import { NobleBN254 } from './precompiles/index.js' import { DefaultBlockchain } from './types.js' import { EVM } from './index.js' -import type { EVMOpts, bn128 } from './index.js' - -let initializedRustBN: bn128 | undefined = undefined +import type { EVMOpts } from './index.js' /** * Use this async static constructor for the initialization @@ -19,8 +17,8 @@ let initializedRustBN: bn128 | undefined = undefined */ export async function createEVM(createOpts?: EVMOpts) { const opts = createOpts ?? ({} as EVMOpts) - const bn128 = initializedRustBN ?? ((await initRustBN()) as bn128) - initializedRustBN = bn128 + + opts.bn254 = new NobleBN254() if (opts.common === undefined) { opts.common = new Common({ chain: Mainnet }) @@ -34,5 +32,5 @@ export async function createEVM(createOpts?: EVMOpts) { opts.stateManager = new SimpleStateManager() } - return new EVM(opts, bn128) + return new EVM(opts) } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 093339bd10..c39855664f 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -42,6 +42,7 @@ import type { Blockchain, CustomOpcode, EVMBLSInterface, + EVMBN254Interface, EVMEvents, EVMInterface, EVMOpts, @@ -49,7 +50,6 @@ import type { EVMRunCallOpts, EVMRunCodeOpts, ExecResult, - bn128, } from './types.js' import type { Common, StateManagerInterface } from '@ethereumjs/common' @@ -143,7 +143,7 @@ export class EVM implements EVMInterface { protected readonly _emit: (topic: string, data: any) => Promise - private _bn128: bn128 + private _bn254: EVMBN254Interface /** * @@ -156,7 +156,7 @@ export class EVM implements EVMInterface { * @param opts The EVM options * @param bn128 Initialized bn128 WASM object for precompile usage (internal) */ - constructor(opts: EVMOpts, bn128: bn128) { + constructor(opts: EVMOpts) { this.common = opts.common! this.blockchain = opts.blockchain! this.stateManager = opts.stateManager! @@ -172,7 +172,6 @@ export class EVM implements EVMInterface { } } - this._bn128 = bn128 this.events = new AsyncEventEmitter() this._optsCached = opts @@ -215,10 +214,12 @@ export class EVM implements EVMInterface { this.getActiveOpcodes() this._precompiles = getActivePrecompiles(this.common, this._customPrecompiles) + // Precompile crypto libraries if (this.common.isActivatedEIP(2537)) { this._bls = opts.bls ?? new NobleBLS() this._bls.init?.() } + this._bn254 = opts.bn254! this._emit = async (topic: string, data: any): Promise => { return new Promise((resolve) => this.events.emit(topic as keyof EVMEvents, data, resolve)) @@ -1085,7 +1086,7 @@ export class EVM implements EVMInterface { stateManager: this.stateManager.shallowCopy(), } ;(opts.stateManager as any).common = common - return new EVM(opts, this._bn128) + return new EVM(opts) } public getPerformanceLogs() { diff --git a/packages/evm/src/exceptions.ts b/packages/evm/src/exceptions.ts index 9959f38dfd..abc2ab269b 100644 --- a/packages/evm/src/exceptions.ts +++ b/packages/evm/src/exceptions.ts @@ -31,6 +31,9 @@ export enum ERROR { BLS_12_381_INPUT_EMPTY = 'input is empty', BLS_12_381_FP_NOT_IN_FIELD = 'fp point not in field', + // BN254 errors + BN254_FP_NOT_IN_FIELD = 'fp point not in field', + // Point Evaluation Errors INVALID_COMMITMENT = 'kzg commitment does not match versioned hash', INVALID_INPUTS = 'kzg inputs invalid', diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts index 94ab69aff6..0733f6c8ba 100644 --- a/packages/evm/src/index.ts +++ b/packages/evm/src/index.ts @@ -6,12 +6,16 @@ import { getOpcodesForHF } from './opcodes/index.js' import { MCLBLS, NobleBLS, + NobleBN254, type PrecompileInput, + RustBN254, getActivePrecompiles, } from './precompiles/index.js' import type { InterpreterStep } from './interpreter.js' import type { + EVMBLSInterface, + EVMBN254Interface, EVMInterface, EVMOpts, EVMResult, @@ -19,12 +23,12 @@ import type { EVMRunCodeOpts, ExecResult, Log, - bn128, } from './types.js' export * from './logger.js' export type { - bn128, + EVMBLSInterface, + EVMBN254Interface, EVMInterface, EVMOpts, EVMResult, @@ -46,6 +50,8 @@ export { MCLBLS, Message, NobleBLS, + NobleBN254, + RustBN254, validateEOF, } diff --git a/packages/evm/src/precompiles/06-ecadd.ts b/packages/evm/src/precompiles/06-ecadd.ts index f66858f280..0c498f15f1 100644 --- a/packages/evm/src/precompiles/06-ecadd.ts +++ b/packages/evm/src/precompiles/06-ecadd.ts @@ -1,14 +1,12 @@ -import { bytesToHex, bytesToUnprefixedHex, hexToBytes, short } from '@ethereumjs/util' +import { bytesToHex, setLengthRight, short } from '@ethereumjs/util' -import { OOGResult } from '../evm.js' +import { EvmErrorResult, OOGResult } from '../evm.js' import type { EVM } from '../evm.js' import type { ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' export function precompile06(opts: PrecompileInput): ExecResult { - const inputData = bytesToUnprefixedHex(opts.data.subarray(0, 128)) - const gasUsed = opts.common.param('ecAddGas') if (opts._debug !== undefined) { opts._debug( @@ -24,7 +22,19 @@ export function precompile06(opts: PrecompileInput): ExecResult { return OOGResult(opts.gasLimit) } - const returnData = hexToBytes((opts._EVM as EVM)['_bn128'].ec_add(inputData)) + // > 128 bytes: chop off extra bytes + // < 128 bytes: right-pad with 0-s + const input = setLengthRight(opts.data.subarray(0, 128), 128) + + let returnData + try { + returnData = (opts._EVM as EVM)['_bn254'].add(input) + } catch (e: any) { + if (opts._debug !== undefined) { + opts._debug(`ECADD (0x06) failed: ${e.message}`) + } + return EvmErrorResult(e, opts.gasLimit) + } // check ecadd success or failure by comparing the output length if (returnData.length !== 64) { diff --git a/packages/evm/src/precompiles/07-ecmul.ts b/packages/evm/src/precompiles/07-ecmul.ts index 6f269f1d41..a1d3dda857 100644 --- a/packages/evm/src/precompiles/07-ecmul.ts +++ b/packages/evm/src/precompiles/07-ecmul.ts @@ -1,13 +1,12 @@ -import { bytesToHex, bytesToUnprefixedHex, hexToBytes, short } from '@ethereumjs/util' +import { bytesToHex, setLengthRight, short } from '@ethereumjs/util' -import { OOGResult } from '../evm.js' +import { EvmErrorResult, OOGResult } from '../evm.js' import type { EVM } from '../evm.js' import type { ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' export function precompile07(opts: PrecompileInput): ExecResult { - const inputData = bytesToUnprefixedHex(opts.data.subarray(0, 128)) const gasUsed = opts.common.param('ecMulGas') if (opts._debug !== undefined) { opts._debug( @@ -24,7 +23,19 @@ export function precompile07(opts: PrecompileInput): ExecResult { return OOGResult(opts.gasLimit) } - const returnData = hexToBytes((opts._EVM as EVM)['_bn128'].ec_mul(inputData)) + // > 128 bytes: chop off extra bytes + // < 128 bytes: right-pad with 0-s + const input = setLengthRight(opts.data.subarray(0, 128), 128) + + let returnData + try { + returnData = (opts._EVM as EVM)['_bn254'].mul(input) + } catch (e: any) { + if (opts._debug !== undefined) { + opts._debug(`ECMUL (0x07) failed: ${e.message}`) + } + return EvmErrorResult(e, opts.gasLimit) + } // check ecmul success or failure by comparing the output length if (returnData.length !== 64) { diff --git a/packages/evm/src/precompiles/08-ecpairing.ts b/packages/evm/src/precompiles/08-ecpairing.ts index aeda2b96ca..3715d5082b 100644 --- a/packages/evm/src/precompiles/08-ecpairing.ts +++ b/packages/evm/src/precompiles/08-ecpairing.ts @@ -1,15 +1,20 @@ -import { bytesToHex, bytesToUnprefixedHex, hexToBytes, short } from '@ethereumjs/util' +import { bytesToHex, short } from '@ethereumjs/util' -import { OOGResult } from '../evm.js' +import { EvmErrorResult, OOGResult } from '../evm.js' +import { ERROR, EvmError } from '../exceptions.js' + +import { moduloLengthCheck } from './util.js' import type { EVM } from '../evm.js' import type { ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' export function precompile08(opts: PrecompileInput): ExecResult { - const inputData = opts.data - // no need to care about non-divisible-by-192, because bn128.pairing will properly fail in that case - const inputDataSize = BigInt(Math.floor(inputData.length / 192)) + if (!moduloLengthCheck(opts, 192, 'ECPAIRING (0x08)')) { + return EvmErrorResult(new EvmError(ERROR.INVALID_INPUT_LENGTH), opts.gasLimit) + } + + const inputDataSize = BigInt(Math.floor(opts.data.length / 192)) const gasUsed = opts.common.param('ecPairingGas') + inputDataSize * opts.common.param('ecPairingWordGas') if (opts._debug !== undefined) { @@ -27,9 +32,15 @@ export function precompile08(opts: PrecompileInput): ExecResult { return OOGResult(opts.gasLimit) } - const returnData = hexToBytes( - (opts._EVM as EVM)['_bn128'].ec_pairing(bytesToUnprefixedHex(inputData)), - ) + let returnData + try { + returnData = (opts._EVM as EVM)['_bn254'].pairing(opts.data) + } catch (e: any) { + if (opts._debug !== undefined) { + opts._debug(`ECPAIRING (0x08) failed: ${e.message}`) + } + return EvmErrorResult(e, opts.gasLimit) + } // check ecpairing success or failure by comparing the output length if (returnData.length !== 32) { diff --git a/packages/evm/src/precompiles/0b-bls12-g1add.ts b/packages/evm/src/precompiles/0b-bls12-g1add.ts index cad13c876e..34be7c5309 100644 --- a/packages/evm/src/precompiles/0b-bls12-g1add.ts +++ b/packages/evm/src/precompiles/0b-bls12-g1add.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { equalityLengthCheck, gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { equalityLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/0c-bls12-g1mul.ts b/packages/evm/src/precompiles/0c-bls12-g1mul.ts index 3302c77295..92dd9ab8ab 100644 --- a/packages/evm/src/precompiles/0c-bls12-g1mul.ts +++ b/packages/evm/src/precompiles/0c-bls12-g1mul.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { equalityLengthCheck, gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { equalityLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/0d-bls12-g1msm.ts b/packages/evm/src/precompiles/0d-bls12-g1msm.ts index e1b00709c0..257265cb27 100644 --- a/packages/evm/src/precompiles/0d-bls12-g1msm.ts +++ b/packages/evm/src/precompiles/0d-bls12-g1msm.ts @@ -3,12 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { - gasCheck, - leading16ZeroBytesCheck, - moduloLengthCheck, - msmGasUsed, -} from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck, msmGasUsed } from './bls12_381/index.js' +import { moduloLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/0e-bls12-g2add.ts b/packages/evm/src/precompiles/0e-bls12-g2add.ts index 12bc94804f..019f815075 100644 --- a/packages/evm/src/precompiles/0e-bls12-g2add.ts +++ b/packages/evm/src/precompiles/0e-bls12-g2add.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { equalityLengthCheck, gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { equalityLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/0f-bls12-g2mul.ts b/packages/evm/src/precompiles/0f-bls12-g2mul.ts index ed4c8553ff..a8817e0a65 100644 --- a/packages/evm/src/precompiles/0f-bls12-g2mul.ts +++ b/packages/evm/src/precompiles/0f-bls12-g2mul.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { equalityLengthCheck, gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { equalityLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/10-bls12-g2msm.ts b/packages/evm/src/precompiles/10-bls12-g2msm.ts index d660b1521d..f637959f38 100644 --- a/packages/evm/src/precompiles/10-bls12-g2msm.ts +++ b/packages/evm/src/precompiles/10-bls12-g2msm.ts @@ -3,12 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { - gasCheck, - leading16ZeroBytesCheck, - moduloLengthCheck, - msmGasUsed, -} from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck, msmGasUsed } from './bls12_381/index.js' +import { moduloLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/11-bls12-pairing.ts b/packages/evm/src/precompiles/11-bls12-pairing.ts index b36cf33049..7d7eb44eed 100644 --- a/packages/evm/src/precompiles/11-bls12-pairing.ts +++ b/packages/evm/src/precompiles/11-bls12-pairing.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { gasCheck, leading16ZeroBytesCheck, moduloLengthCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { moduloLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/12-bls12-map-fp-to-g1.ts b/packages/evm/src/precompiles/12-bls12-map-fp-to-g1.ts index 28eb2b9168..f95792d895 100644 --- a/packages/evm/src/precompiles/12-bls12-map-fp-to-g1.ts +++ b/packages/evm/src/precompiles/12-bls12-map-fp-to-g1.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { equalityLengthCheck, gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { equalityLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/13-bls12-map-fp2-to-g2.ts b/packages/evm/src/precompiles/13-bls12-map-fp2-to-g2.ts index b774cb098f..2e49d12a18 100644 --- a/packages/evm/src/precompiles/13-bls12-map-fp2-to-g2.ts +++ b/packages/evm/src/precompiles/13-bls12-map-fp2-to-g2.ts @@ -3,7 +3,8 @@ import { bytesToHex } from '@ethereumjs/util' import { EvmErrorResult, OOGResult } from '../evm.js' import { ERROR, EvmError } from '../exceptions.js' -import { equalityLengthCheck, gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { gasCheck, leading16ZeroBytesCheck } from './bls12_381/index.js' +import { equalityLengthCheck } from './util.js' import type { EVMBLSInterface, ExecResult } from '../types.js' import type { PrecompileInput } from './types.js' diff --git a/packages/evm/src/precompiles/bls12_381/noble.ts b/packages/evm/src/precompiles/bls12_381/noble.ts index 4a39590785..31737d5d40 100644 --- a/packages/evm/src/precompiles/bls12_381/noble.ts +++ b/packages/evm/src/precompiles/bls12_381/noble.ts @@ -66,7 +66,8 @@ function BLS12_381_FromG1Point(input: AffinePoint): Uint8Array { * @param input Input Uint8Array. Should be 256 bytes * @returns Noble G2 point */ -function BLS12_381_ToG2Point(input: Uint8Array) { +function BLS12_381_ToG2Point(input: Uint8Array): any { + // TODO: remove any type, temporary fix due to conflicing @noble/curves versions if (equalsBytes(input, BLS_G2_INFINITY_POINT_BYTES)) { return bls12_381.G2.ProjectivePoint.ZERO } @@ -141,7 +142,8 @@ function BLS12_381_ToFpPoint(fpCoordinate: Uint8Array) { return FP } -function BLS12_381_ToFp2Point(fpXCoordinate: Uint8Array, fpYCoordinate: Uint8Array) { +function BLS12_381_ToFp2Point(fpXCoordinate: Uint8Array, fpYCoordinate: Uint8Array): any { + // TODO: remove any type, temporary fix due to conflicing @noble/curves versions // check if the coordinates are in the field if (bytesToBigInt(fpXCoordinate) >= BLS_FIELD_MODULUS) { throw new EvmError(ERROR.BLS_12_381_FP_NOT_IN_FIELD) @@ -291,6 +293,7 @@ export class NobleBLS implements EVMBLSInterface { } pairingCheck(input: Uint8Array): Uint8Array { + // Extract the pairs from the input const pairLength = 384 const pairs = [] for (let k = 0; k < input.length / pairLength; k++) { diff --git a/packages/evm/src/precompiles/bls12_381/util.ts b/packages/evm/src/precompiles/bls12_381/util.ts index 618acca0a7..9d1b6b2a94 100644 --- a/packages/evm/src/precompiles/bls12_381/util.ts +++ b/packages/evm/src/precompiles/bls12_381/util.ts @@ -56,47 +56,6 @@ export const msmGasUsed = (numPairs: number, gasUsedPerPair: bigint) => { return (BigInt(numPairs) * gasUsedPerPair * BigInt(gasDiscountMultiplier)) / BigInt(1000) } -/** - * Checks that the length of the provided data is equal to `length`. - * - * @param opts - * @param length - * @param pName - * @returns - */ -export const equalityLengthCheck = (opts: PrecompileInput, length: number, pName: string) => { - if (opts.data.length !== length) { - if (opts._debug !== undefined) { - opts._debug( - `${pName} failed: Invalid input length length=${opts.data.length} (expected: ${length})`, - ) - } - return false - } - return true -} - -/** - * Checks that the total length of the provided data input can be subdivided into k equal parts - * with `length` (without leaving some remainder bytes). - * - * @param opts - * @param length - * @param pName - * @returns - */ -export const moduloLengthCheck = (opts: PrecompileInput, length: number, pName: string) => { - if (opts.data.length % length !== 0) { - if (opts._debug !== undefined) { - opts._debug( - `${pName} failed: Invalid input length length=${opts.data.length} (expected: ${length}*k bytes)`, - ) - } - return false - } - return true -} - /** * BLS-specific zero check to check that the top 16 bytes of a 64 byte field element provided * are always zero (see EIP notes on field element encoding). diff --git a/packages/evm/src/precompiles/bn254/index.ts b/packages/evm/src/precompiles/bn254/index.ts new file mode 100644 index 0000000000..d54b65c7e0 --- /dev/null +++ b/packages/evm/src/precompiles/bn254/index.ts @@ -0,0 +1,2 @@ +export { NobleBN254 } from './noble.js' +export { RustBN254 } from './rustbn.js' diff --git a/packages/evm/src/precompiles/bn254/noble.ts b/packages/evm/src/precompiles/bn254/noble.ts new file mode 100644 index 0000000000..5f3396602d --- /dev/null +++ b/packages/evm/src/precompiles/bn254/noble.ts @@ -0,0 +1,168 @@ +import { + BIGINT_0, + bigIntToBytes, + bytesToBigInt, + concatBytes, + equalsBytes, + hexToBytes, + setLengthLeft, +} from '@ethereumjs/util' +import { bn254 } from '@noble/curves/bn254' + +import { ERROR, EvmError } from '../../exceptions.js' + +import type { EVMBN254Interface } from '../../types.js' +import type { AffinePoint } from '@noble/curves/abstract/weierstrass' + +const G1_INFINITY_POINT_BYTES = new Uint8Array(64) +const G2_INFINITY_POINT_BYTES = new Uint8Array(128) +const G1_POINT_BYTE_LENGTH = 64 +const G1_ELEMENT_BYTE_LENGTH = 32 +const G2_POINT_BYTE_LENGTH = 128 + +const ZERO_BUFFER = new Uint8Array(32) +const ONE_BUFFER = concatBytes(new Uint8Array(31), hexToBytes('0x01')) + +/** + * Converts an Uint8Array to a Noble G1 point. + * @param input Input Uint8Array. Should be 64 bytes + * @returns Noble G1 point + */ +function toG1Point(input: Uint8Array) { + if (equalsBytes(input, G1_INFINITY_POINT_BYTES)) { + return bn254.G1.ProjectivePoint.ZERO + } + + const x = bytesToBigInt(input.subarray(0, G1_ELEMENT_BYTE_LENGTH)) + const y = bytesToBigInt(input.subarray(G1_ELEMENT_BYTE_LENGTH, G1_POINT_BYTE_LENGTH)) + + const G1 = bn254.G1.ProjectivePoint.fromAffine({ + x, + y, + }) + G1.assertValidity() + return G1 +} + +function fromG1Point(input: AffinePoint): Uint8Array { + const xBytes = setLengthLeft(bigIntToBytes(input.x), G1_ELEMENT_BYTE_LENGTH) + const yBytes = setLengthLeft(bigIntToBytes(input.y), G1_ELEMENT_BYTE_LENGTH) + + return concatBytes(xBytes, yBytes) +} + +// input: a 32-byte hex scalar Uint8Array +// output: a Noble Fr point + +function toFrPoint(input: Uint8Array): bigint { + const Fr = bn254.fields.Fr.fromBytes(input) + if (Fr >= bn254.fields.Fr.ORDER) { + return Fr % bn254.fields.Fr.ORDER + } + return Fr +} + +/** + * Converts an Uint8Array to a Noble G2 point. Raises errors if the point is not on the curve + * and (if activated) if the point is in the subgroup / order check. + * @param input Input Uint8Array. Should be 256 bytes + * @returns Noble G2 point + */ +function toG2Point(input: Uint8Array): any { + // TODO: remove any type, temporary fix due to conflicing @noble/curves versions + if (equalsBytes(input, G2_INFINITY_POINT_BYTES)) { + return bn254.G2.ProjectivePoint.ZERO + } + + const p_x_2 = input.subarray(0, G1_ELEMENT_BYTE_LENGTH) + const p_x_1 = input.subarray(G1_ELEMENT_BYTE_LENGTH, G1_ELEMENT_BYTE_LENGTH * 2) + const start2 = G1_ELEMENT_BYTE_LENGTH * 2 + const p_y_2 = input.subarray(start2, start2 + G1_ELEMENT_BYTE_LENGTH) + const p_y_1 = input.subarray(start2 + G1_ELEMENT_BYTE_LENGTH, start2 + G1_ELEMENT_BYTE_LENGTH * 2) + + for (const p of [p_x_1, p_x_2, p_y_1, p_y_2]) { + const pB = bytesToBigInt(p) + if (bn254.fields.Fp.create(pB) !== pB) { + throw new EvmError(ERROR.BN254_FP_NOT_IN_FIELD) + } + } + + const Fp2X = toFp2Point(p_x_1, p_x_2) + const Fp2Y = toFp2Point(p_y_1, p_y_2) + + const pG2 = bn254.G2.ProjectivePoint.fromAffine({ + x: Fp2X, + y: Fp2Y, + }) + + pG2.assertValidity() + + return pG2 +} + +function toFp2Point(fpXCoordinate: Uint8Array, fpYCoordinate: Uint8Array) { + if (bytesToBigInt(fpXCoordinate) >= bn254.fields.Fp2.ORDER) { + throw new EvmError(ERROR.BN254_FP_NOT_IN_FIELD) + } + if (bytesToBigInt(fpYCoordinate) >= bn254.fields.Fp2.ORDER) { + throw new EvmError(ERROR.BN254_FP_NOT_IN_FIELD) + } + + const fpBytes = concatBytes(fpXCoordinate, fpYCoordinate) + + const FP = bn254.fields.Fp2.fromBytes(fpBytes) + return FP +} + +/** + * Implementation of the `EVMBN254Interface` using the `@noble/curves` JS library, + * see https://github.com/paulmillr/noble-curves. + * + * This is the EVM default implementation. + */ +export class NobleBN254 implements EVMBN254Interface { + add(input: Uint8Array): Uint8Array { + const p1 = toG1Point(input.slice(0, G1_POINT_BYTE_LENGTH)) + const p2 = toG1Point(input.slice(G1_POINT_BYTE_LENGTH, G1_POINT_BYTE_LENGTH * 2)) + + const result = fromG1Point(p1.add(p2)) + return result + } + + mul(input: Uint8Array): Uint8Array { + const p1 = toG1Point(input.slice(0, G1_POINT_BYTE_LENGTH)) + const scalar = toFrPoint(input.slice(G1_POINT_BYTE_LENGTH, 96)) + + if (scalar === BIGINT_0) { + return G1_INFINITY_POINT_BYTES + } + + const result = fromG1Point(p1.multiply(scalar)) + return result + } + pairing(input: Uint8Array): Uint8Array { + // Extract the pairs from the input + const pairLength = 192 + const pairs = [] + for (let k = 0; k < input.length / pairLength; k++) { + const pairStart = pairLength * k + const G1 = toG1Point(input.subarray(pairStart, pairStart + G1_POINT_BYTE_LENGTH)) + + const g2start = pairStart + G1_POINT_BYTE_LENGTH + const G2 = toG2Point(input.subarray(g2start, g2start + G2_POINT_BYTE_LENGTH)) + + if (G1 === bn254.G1.ProjectivePoint.ZERO || G2 === bn254.G2.ProjectivePoint.ZERO) { + continue + } + + pairs.push({ g1: G1, g2: G2 }) + } + + const res = bn254.pairingBatch(pairs) + if (bn254.fields.Fp12.eql(res, bn254.fields.Fp12.ONE) === true) { + return ONE_BUFFER + } else { + return ZERO_BUFFER + } + } +} diff --git a/packages/evm/src/precompiles/bn254/rustbn.ts b/packages/evm/src/precompiles/bn254/rustbn.ts new file mode 100644 index 0000000000..a6a9877d77 --- /dev/null +++ b/packages/evm/src/precompiles/bn254/rustbn.ts @@ -0,0 +1,32 @@ +import { bytesToUnprefixedHex, hexToBytes } from '@ethereumjs/util' + +import type { EVMBN254Interface } from '../../types.js' + +/** + * Implementation of the `EVMBN254Interface` using a WASM wrapper https://github.com/ethereumjs/rustbn.js + * around the Parity fork of the Zcash bn pairing cryptography library. + * + * This can be optionally used to replace the build-in Noble implementation (`NobleBN254`) with + * a more performant WASM variant. See EVM `bls` constructor option on how to use. + */ +export class RustBN254 implements EVMBN254Interface { + protected readonly _rustbn: any + + constructor(rustbn: any) { + this._rustbn = rustbn + } + + add(input: Uint8Array): Uint8Array { + const inputStr = bytesToUnprefixedHex(input) + return hexToBytes(this._rustbn.ec_add(inputStr)) + } + + mul(input: Uint8Array): Uint8Array { + const inputHex = bytesToUnprefixedHex(input) + return hexToBytes(this._rustbn.ec_mul(inputHex)) + } + pairing(input: Uint8Array): Uint8Array { + const inputStr = bytesToUnprefixedHex(input) + return hexToBytes(this._rustbn.ec_pairing(inputStr)) + } +} diff --git a/packages/evm/src/precompiles/index.ts b/packages/evm/src/precompiles/index.ts index c84f429948..c2ec81b8dc 100644 --- a/packages/evm/src/precompiles/index.ts +++ b/packages/evm/src/precompiles/index.ts @@ -21,6 +21,7 @@ import { precompile11 } from './11-bls12-pairing.js' import { precompile12 } from './12-bls12-map-fp-to-g1.js' import { precompile13 } from './13-bls12-map-fp2-to-g2.js' import { MCLBLS, NobleBLS } from './bls12_381/index.js' +import { NobleBN254, RustBN254 } from './bn254/index.js' import type { PrecompileFunc, PrecompileInput } from './types.js' import type { Common } from '@ethereumjs/common' @@ -307,9 +308,11 @@ export { getPrecompileName, MCLBLS, NobleBLS, + NobleBN254, precompileEntries, precompiles, ripemdPrecompileAddress, + RustBN254, } export type { AddPrecompile, CustomPrecompile, DeletePrecompile, PrecompileFunc, PrecompileInput } diff --git a/packages/evm/src/precompiles/util.ts b/packages/evm/src/precompiles/util.ts new file mode 100644 index 0000000000..e8dc82d306 --- /dev/null +++ b/packages/evm/src/precompiles/util.ts @@ -0,0 +1,42 @@ +import type { PrecompileInput } from './index.js' + +/** + * Checks that the length of the provided data is equal to `length`. + * + * @param opts + * @param length + * @param pName + * @returns + */ +export const equalityLengthCheck = (opts: PrecompileInput, length: number, pName: string) => { + if (opts.data.length !== length) { + if (opts._debug !== undefined) { + opts._debug( + `${pName} failed: Invalid input length length=${opts.data.length} (expected: ${length})`, + ) + } + return false + } + return true +} + +/** + * Checks that the total length of the provided data input can be subdivided into k equal parts + * with `length` (without leaving some remainder bytes). + * + * @param opts + * @param length + * @param pName + * @returns + */ +export const moduloLengthCheck = (opts: PrecompileInput, length: number, pName: string) => { + if (opts.data.length % length !== 0) { + if (opts._debug !== undefined) { + opts._debug( + `${pName} failed: Invalid input length length=${opts.data.length} (expected: ${length}*k bytes)`, + ) + } + return false + } + return true +} diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 109b79129d..752836afc0 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -284,7 +284,7 @@ export interface EVMOpts { /** * For the EIP-2935 BLS precompiles, the native JS `@noble/curves` * https://github.com/paulmillr/noble-curves BLS12-381 curve implementation - * is used (see `noble.ts` file in the `precompiles` folder). + * is used (see `noble.ts` file in the `precompiles/bls12_381/` folder). * * To use an alternative implementation this option can be used by passing * in a wrapper implementation integrating the desired library and adhering @@ -303,6 +303,29 @@ export interface EVMOpts { */ bls?: EVMBLSInterface + /** + * For the EIP-196/EIP-197 BN254 (alt_BN128) EC precompiles, the native JS `@noble/curves` + * https://github.com/paulmillr/noble-curves BN254 curve implementation + * is used (see `noble.ts` file in the `precompiles/bn254/` folder). + * + * To use an alternative implementation this option can be used by passing + * in a wrapper implementation integrating the desired library and adhering + * to the `EVMBN254Interface` specification. + * + * An interface for a WASM wrapper https://github.com/ethereumjs/rustbn.js around the + * Parity fork of the Zcash bn pairing cryptography library is shipped with this library + * which can be used as follows (with `rustbn.js` being explicitly added to the set of + * dependencies): + * + * ```ts + * import { initRustBN } from 'rustbn-wasm' + * + * const bn254 = await initRustBN() + * const evm = await createEVM({ bn254: new RustBN254(bn254) }) + * ``` + */ + bn254?: EVMBN254Interface + /* * The EVM comes with a basic dependency-minimized `SimpleStateManager` implementation * which serves most code execution use cases and which is included in the @@ -382,6 +405,10 @@ export interface ExecResult { blobGasUsed?: bigint } +/** + * High level wrapper for BLS libraries used + * for the BLS precompiles + */ export type EVMBLSInterface = { init?(): void addG1(input: Uint8Array): Uint8Array @@ -395,6 +422,16 @@ export type EVMBLSInterface = { pairingCheck(input: Uint8Array): Uint8Array } +/** + * High level wrapper for BN254 (alt_BN128) libraries + * used for the BN254 (alt_BN128) EC precompiles + */ +export type EVMBN254Interface = { + add: (input: Uint8Array) => Uint8Array + mul: (input: Uint8Array) => Uint8Array + pairing: (input: Uint8Array) => Uint8Array +} + /** * Log that the contract emits. */ @@ -446,15 +483,6 @@ export class DefaultBlockchain implements Blockchain { } } -/** - * The BN128 curve package (`rustbn-wasm`) - */ -export interface bn128 { - ec_pairing: (input_str: string) => PrefixedHexString - ec_add: (input_str: string) => PrefixedHexString - ec_mul: (input_hex: string) => PrefixedHexString -} - // EOF type which holds the execution-related data for EOF export type EOFEnv = { container: EOFContainer diff --git a/packages/vm/test/tester/index.ts b/packages/vm/test/tester/index.ts index 864c22eaac..13061b2e60 100755 --- a/packages/vm/test/tester/index.ts +++ b/packages/vm/test/tester/index.ts @@ -1,4 +1,4 @@ -import { MCLBLS, NobleBLS, type bn128 } from '@ethereumjs/evm' +import { MCLBLS, NobleBLS, NobleBN254, RustBN254 } from '@ethereumjs/evm' import { loadKZG } from 'kzg-wasm' import * as mcl from 'mcl-wasm' import * as minimist from 'minimist' @@ -21,7 +21,7 @@ import { runStateTest } from './runners/GeneralStateTestsRunner.js' import { getTestFromSource, getTestsFromArgs } from './testLoader.js' import type { Common } from '@ethereumjs/common' -import type { EVMBLSInterface } from '@ethereumjs/evm/dist/cjs/types' +import type { EVMBLSInterface, EVMBN254Interface } from '@ethereumjs/evm' /** * Test runner @@ -48,6 +48,7 @@ import type { EVMBLSInterface } from '@ethereumjs/evm/dist/cjs/types' * --verify-test-amount-alltests: number. If passed, get the expected amount from tests and verify afterwards if this is the count of tests (expects tests are ran with default settings) * --reps: number. If passed, each test case will be run the number of times indicated * --bls: string. BLS library being used, choices: Noble, MCL (default: MCL) + * --bn254: string. BN254 (alt_BN128) library being used, choices: Noble, RustBN (default: RustBN) * --profile If this flag is passed, the state/blockchain tests will profile */ @@ -112,11 +113,20 @@ async function runTests() { console.log('BLS library used: MCL (WASM)') } + let bn254: EVMBN254Interface + if (argv.bn254 !== undefined && argv.bn254.toLowerCase() === 'noble') { + console.log('BN254 (alt_BN128) library used: Noble (JavaScript)') + bn254 = new NobleBN254() + } else { + const rustBN = await initRustBN() + bn254 = new RustBN254(rustBN) + console.log('BN254 (alt_BN128) library used: rustbn.js (WASM)') + } + /** * Run-time configuration */ const kzg = await loadKZG() - const bn128 = (await initRustBN()) as bn128 const runnerArgs: { forkConfigVM: string forkConfigTestSuite: string @@ -130,7 +140,7 @@ async function runTests() { reps?: number profile: boolean bls: EVMBLSInterface - bn128: bn128 + bn254: EVMBN254Interface } = { forkConfigVM: FORK_CONFIG_VM, forkConfigTestSuite: FORK_CONFIG_TEST_SUITE, @@ -144,7 +154,7 @@ async function runTests() { reps: argv.reps, // test repetitions bls, profile: RUN_PROFILER, - bn128, + bn254, } /** diff --git a/packages/vm/test/tester/runners/BlockchainTestsRunner.ts b/packages/vm/test/tester/runners/BlockchainTestsRunner.ts index 013bfd3582..92e20239fd 100644 --- a/packages/vm/test/tester/runners/BlockchainTestsRunner.ts +++ b/packages/vm/test/tester/runners/BlockchainTestsRunner.ts @@ -92,6 +92,7 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes const evmOpts = { bls: options.bls, + bn254: options.bn254, } let vm = await VM.create({ stateManager, @@ -147,17 +148,14 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes try { const blockRlp = hexToBytes(raw.rlp as PrefixedHexString) // Update common HF - let TD: bigint | undefined = undefined let timestamp: bigint | undefined = undefined try { const decoded: any = RLP.decode(blockRlp) - const parentHash = decoded[0][0] - TD = await blockchain.getTotalDifficulty(parentHash) timestamp = bytesToBigInt(decoded[0][11]) // eslint-disable-next-line no-empty } catch (e) {} - common.setHardforkBy({ blockNumber: currentBlock, td: TD, timestamp }) + common.setHardforkBy({ blockNumber: currentBlock, timestamp }) // transactionSequence is provided when txs are expected to be rejected. // To run this field we try to import them on the current state. @@ -191,7 +189,7 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes await blockBuilder.revert() // will only revert if checkpointed } - const block = createBlockFromRLPSerializedBlock(blockRlp, { common, setHardfork: TD }) + const block = createBlockFromRLPSerializedBlock(blockRlp, { common }) await blockchain.putBlock(block) // This is a trick to avoid generating the canonical genesis @@ -206,7 +204,7 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes const parentState = parentBlock.header.stateRoot // run block, update head if valid try { - await runBlock(vm, { block, root: parentState, setHardfork: TD }) + await runBlock(vm, { block, root: parentState }) // set as new head block } catch (error: any) { // remove invalid block diff --git a/packages/vm/test/tester/runners/GeneralStateTestsRunner.ts b/packages/vm/test/tester/runners/GeneralStateTestsRunner.ts index 19219e473e..992cc6c11c 100644 --- a/packages/vm/test/tester/runners/GeneralStateTestsRunner.ts +++ b/packages/vm/test/tester/runners/GeneralStateTestsRunner.ts @@ -87,6 +87,7 @@ async function runTestCase(options: any, testData: any, t: tape.Test) { const evmOpts = { bls: options.bls, + bn254: options.bn254, } const vm = await VM.create({ stateManager,