UNPKG

@unruggable/gateways

Version:

Trustless Ethereum Multichain CCIP-Read Gateway

1,012 lines (1,011 loc) 37.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlockProver = exports.AbstractProver = exports.GatewayVM = exports.GatewayRequest = exports.GatewayProgram = void 0; exports.solidityArraySlots = solidityArraySlots; exports.solidityFollowSlot = solidityFollowSlot; exports.pow256 = pow256; exports.isTargetNeed = isTargetNeed; exports.requireV1Needs = requireV1Needs; exports.makeStorageKey = makeStorageKey; const constants_1 = require("ethers/constants"); const contract_1 = require("ethers/contract"); const abi_1 = require("ethers/abi"); const crypto_1 = require("ethers/crypto"); const hash_1 = require("ethers/hash"); const utils_1 = require("ethers/utils"); const ezccip_1 = require("@namestone/ezccip"); const wrap_js_1 = require("./wrap.cjs"); const utils_js_1 = require("./utils.cjs"); const cached_js_1 = require("./cached.cjs"); const ops_js_1 = require("./ops.cjs"); const reader_js_1 = require("./reader.cjs"); async function peekSize(value) { if (value instanceof wrap_js_1.Wrapped) { if (value.payload) return value.payload; value = await value.get(); } return (value.length - 2) >> 1; } // EVAL_LOOP flags // the following should be equivalent to GatewayRequest.sol const STOP_ON_SUCCESS = 1 << 0; const STOP_ON_FAILURE = 1 << 1; const ACQUIRE_STATE = 1 << 2; const KEEP_ARGS = 1 << 3; function isZeros(hex) { return /^0x0*$/.test(hex); } function uint256FromHex(hex) { // the following should be equivalent to: // GatewayVM.stackAsUint256() return hex === '0x' ? 0n : BigInt(hex.slice(0, 66)); } function numberFromHex(hex) { const u = uint256FromHex(hex); if (u > 0xffffff) throw new Error('numeric overflow'); return Number(u); } function addressFromHex(hex) { // the following should be equivalent to: // address(uint160(GatewayVM.stackAsUint256())) return ('0x' + (hex.length >= 66 ? hex.slice(26, 66) : hex.slice(2).padStart(40, '0').slice(-40)).toLowerCase()); } function bigintRange(start, length) { return Array.from({ length }, (_, i) => start + BigInt(i)); } function solidityArraySlots(slot, length) { return length ? bigintRange(BigInt((0, hash_1.solidityPackedKeccak256)(['uint256'], [slot])), length) : []; } function solidityFollowSlot(slot, key) { // https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays return BigInt((0, crypto_1.keccak256)((0, utils_1.concat)([key, (0, utils_js_1.toPaddedHex)(slot)]))); } function pow256(base, exp) { let res = 1n; while (exp) { if (exp & 1n) res = BigInt.asUintN(256, res * base); exp >>= 1n; base = BigInt.asUintN(256, base * base); } return res; } class GatewayProgram { ops; static Opcode = ops_js_1.GATEWAY_OP; constructor(ops = []) { this.ops = ops; } clone() { return new GatewayProgram(this.ops.slice()); } op(key) { return this.addByte(ops_js_1.GATEWAY_OP[key]); // experimental } addByte(x) { if ((x & 0xff) !== x) throw new Error(`expected byte: ${x}`); this.ops.push(x); return this; } addBytes(v) { this.ops.push(...v); return this; } toTuple() { return [this.encode()]; } encode() { return Uint8Array.from(this.ops); } debug(label = '') { const v = (0, utils_1.toUtf8Bytes)(label); return this.addByte(ops_js_1.GATEWAY_OP.DEBUG).addByte(v.length).addBytes(v); } read(n = 1) { return n == 1 ? this.addByte(ops_js_1.GATEWAY_OP.READ_SLOT) : this.push(n).addByte(ops_js_1.GATEWAY_OP.READ_SLOTS); } readBytes() { return this.addByte(ops_js_1.GATEWAY_OP.READ_BYTES); } readHashedBytes() { return this.addByte(ops_js_1.GATEWAY_OP.READ_HASHED_BYTES); } readArray(step) { return this.push(step).addByte(ops_js_1.GATEWAY_OP.READ_ARRAY); } setTarget(x) { return this.push(x).target(); } target() { return this.addByte(ops_js_1.GATEWAY_OP.SET_TARGET); } setOutput(i) { return this.push(i).output(); } output() { return this.addByte(ops_js_1.GATEWAY_OP.SET_OUTPUT); } eval() { return this.push(true).evalIf(); } evalIf() { return this.addByte(ops_js_1.GATEWAY_OP.EVAL); } evalLoop(opts = {}) { let flags = 0; if (opts.success) flags |= STOP_ON_SUCCESS; if (opts.failure) flags |= STOP_ON_FAILURE; if (opts.acquire) flags |= ACQUIRE_STATE; if (opts.keep) flags |= KEEP_ARGS; // TODO: add recursion limit // TODO: add can modify output return this.push(opts.count ?? 255) // this should be >= MAX_STACK .addByte(ops_js_1.GATEWAY_OP.EVAL_LOOP) .addByte(flags); } exit(exitCode) { return this.push(false).assertNonzero(exitCode); } assertNonzero(exitCode) { return this.addByte(ops_js_1.GATEWAY_OP.ASSERT).addByte(exitCode); } requireContract(exitCode = 1) { return this.isContract().assertNonzero(exitCode); // NOTE: does not consume stack } requireNonzero(exitCode = 1) { return this.dup().assertNonzero(exitCode); // NOTE: does not consume stack } setSlot(x) { return this.push(x).slot(); } offset(x) { return this.push(x).addSlot(); } addSlot() { return this.addByte(ops_js_1.GATEWAY_OP.ADD_SLOT); } slot() { return this.addByte(ops_js_1.GATEWAY_OP.SET_SLOT); } follow() { return this.addByte(ops_js_1.GATEWAY_OP.FOLLOW); } followIndex() { return this.getSlot().keccak().slot().addSlot(); } pop() { return this.addByte(ops_js_1.GATEWAY_OP.POP); } dup(back = 0) { return this.push(back).addByte(ops_js_1.GATEWAY_OP.DUP); } swap(back = 1) { return this.push(back).addByte(ops_js_1.GATEWAY_OP.SWAP); } pushOutput(i) { return this.push(i).addByte(ops_js_1.GATEWAY_OP.PUSH_OUTPUT); } pushStack(i) { return this.push(i).addByte(ops_js_1.GATEWAY_OP.PUSH_STACK); } push(x) { const i = BigInt.asUintN(256, BigInt(x)); if (!i) return this.addByte(ops_js_1.GATEWAY_OP.PUSH_0); const s = i.toString(16); const v = (0, utils_1.getBytes)((s.length & 1 ? '0x0' : '0x') + s); this.ops.push(ops_js_1.GATEWAY_OP.PUSH_0 + v.length, ...v); return this; } pushStr(s) { return this.pushBytes((0, utils_1.toUtf8Bytes)(s)); } pushBytes(v) { const u = (0, utils_1.getBytes)(v); return this.addByte(ops_js_1.GATEWAY_OP.PUSH_BYTES).push(u.length).addBytes(u); } pushProgram(program) { return this.pushBytes(program.encode()); } getSlot() { return this.addByte(ops_js_1.GATEWAY_OP.GET_SLOT); } getTarget() { return this.addByte(ops_js_1.GATEWAY_OP.GET_TARGET); } stackSize() { return this.addByte(ops_js_1.GATEWAY_OP.STACK_SIZE); } isContract() { return this.addByte(ops_js_1.GATEWAY_OP.IS_CONTRACT); } concat() { return this.addByte(ops_js_1.GATEWAY_OP.CONCAT); } keccak() { return this.addByte(ops_js_1.GATEWAY_OP.KECCAK); } slice(x, n) { return this.push(x).push(n).addByte(ops_js_1.GATEWAY_OP.SLICE); } length() { return this.addByte(ops_js_1.GATEWAY_OP.LENGTH); } plus() { return this.addByte(ops_js_1.GATEWAY_OP.PLUS); } twosComplement() { return this.not().push(1).plus(); } subtract() { return this.twosComplement().plus(); } times() { return this.addByte(ops_js_1.GATEWAY_OP.TIMES); } divide() { return this.addByte(ops_js_1.GATEWAY_OP.DIVIDE); } mod() { return this.addByte(ops_js_1.GATEWAY_OP.MOD); } pow() { return this.addByte(ops_js_1.GATEWAY_OP.POW); } and() { return this.addByte(ops_js_1.GATEWAY_OP.AND); } or() { return this.addByte(ops_js_1.GATEWAY_OP.OR); } xor() { return this.addByte(ops_js_1.GATEWAY_OP.XOR); } isZero() { return this.addByte(ops_js_1.GATEWAY_OP.IS_ZERO); } not() { return this.addByte(ops_js_1.GATEWAY_OP.NOT); } shl(shift) { return this.push(shift).addByte(ops_js_1.GATEWAY_OP.SHIFT_LEFT); } shr(shift) { return this.push(shift).addByte(ops_js_1.GATEWAY_OP.SHIFT_RIGHT); } eq() { return this.addByte(ops_js_1.GATEWAY_OP.EQ); } lt() { return this.addByte(ops_js_1.GATEWAY_OP.LT); } gt() { return this.addByte(ops_js_1.GATEWAY_OP.GT); } neq() { return this.eq().isZero(); } lte() { return this.gt().isZero(); } gte() { return this.lt().isZero(); } dup2() { // [a, b] => [a, b, a] => [a, b, a, b] return this.dup(1).dup(1); } min() { //return this.dup2().gt().addByte(OP.SWAP).pop(); return this.dup().dup(2).lt().addByte(ops_js_1.GATEWAY_OP.SWAP).pop(); } max() { return this.dup().dup(2).gt().addByte(ops_js_1.GATEWAY_OP.SWAP).pop(); } } exports.GatewayProgram = GatewayProgram; // a request is just a program where the leading byte is the number of outputs class GatewayRequest extends GatewayProgram { constructor(outputCount = 0) { super(); this.addByte(outputCount); } clone() { const temp = new GatewayRequest(); temp.ops.length = 0; temp.ops.push(...this.ops); return temp; } get outputCount() { return this.ops[0]; } // the following functionality is not available in solidity! ensureCapacity(n) { if (n < this.outputCount) throw new Error('invalid capacity'); if (n > 0xff) throw new Error('output overflow'); this.ops[0] = n; } // convenience for writing requests addOutput() { const i = this.outputCount; this.ensureCapacity(i + 1); return this.setOutput(i); } // convenience for draining stack into outputs drain(count) { const i = this.outputCount; this.ensureCapacity(i + count); while (count > 0) this.setOutput(i + --count); return this; } } exports.GatewayRequest = GatewayRequest; function isTargetNeed(need) { return typeof need === 'object' && need && 'target' in need; } function requireV1Needs(needs) { if (!needs.length) { throw new Error('expected needs'); } const need = needs[0]; if (!isTargetNeed(need)) { throw new Error('first need must be account'); } const slots = needs.slice(1).map((need) => { if (typeof need !== 'bigint') { throw new Error('remaining needs must be storage'); } return need; }); return { ...need, slots }; } // record the state of an evaluation // registers: [slot, target, stack] + exitCode // outputs are shared across eval() // needs is sequence of necessary proofs class GatewayVM { outputs; maxStack; allocBudget; needs; targets; static create(outputCount, maxStackSize = Infinity, allocBudget = Infinity) { return new this(Array(outputCount).fill('0x'), maxStackSize, allocBudget); } target = constants_1.ZeroAddress; slot = 0n; stack = []; exitCode = 0; constructor(outputs, maxStack, allocBudget, needs = [], targets = new Map()) { this.outputs = outputs; this.maxStack = maxStack; this.allocBudget = allocBudget; this.needs = needs; this.targets = targets; } checkAlloc(size) { this.allocBudget -= size; if (this.allocBudget < 0) throw new Error('too much allocation'); } checkOutputIndex(i) { if (i >= this.outputs.length) { throw new Error(`invalid output index: ${i}/${this.outputs.length}`); } return i; } checkStackIndex(i) { if (i < 0 || i >= this.stack.length) { throw new Error(`invalid stack index: ${i}/${this.stack.length}`); } return i; } checkBack(back) { return this.checkStackIndex(this.stack.length - 1 - back); } resolveOutputs() { return Promise.all(this.outputs.map(wrap_js_1.unwrap)); } resolveStack() { return Promise.all(this.stack.map(wrap_js_1.unwrap)); } push(x) { if (this.stack.length >= this.maxStack) throw new Error('stack overflow'); this.stack.push(x); } pushUint256(x) { this.push((0, utils_js_1.toPaddedHex)(x)); } pop() { if (!this.stack.length) throw new Error('stack underflow'); return this.stack.pop(); } popSlice(n) { if (this.stack.length < n) throw new Error('stack underflow'); return this.stack.splice(this.stack.length - n, n); } async popNumber() { return numberFromHex(await (0, wrap_js_1.unwrap)(this.pop())); } async binaryOp(fn) { const [a, b] = await Promise.all(this.popSlice(2).map(wrap_js_1.unwrap)); this.pushUint256(fn(uint256FromHex(a), uint256FromHex(b))); } } exports.GatewayVM = GatewayVM; function checkSize(size, limit) { if (size > limit) throw new Error(`too many bytes: ${size} > ${limit}`); return Number(size); } const GATEWAY_EXT_ABI = new abi_1.Interface([ // ReadBytesAt.sol 'function readBytesAt(uint256 slot) view returns (bytes)', ]); // standard caching protocol: // account proofs stored under 0x{HexAddress} // storage proofs stored under 0x{HexAddress}{HexSlot w/NoZeroPad} via makeStorageKey() function makeStorageKey(target, slot) { return `${target}${slot.toString(16)}`; } class AbstractProver { provider; // general proof cache proofLRU = new cached_js_1.LRU(10000); // general async cache // default: deduplicates in-flight but does not cache cache = new cached_js_1.CachedMap(0); // remember if contract supports readBytesAt() readBytesAtSupported = new Map(); // maximum number of items on stack // should not be larger than MAX_STACK in GatewayProtocol.sol maxStackSize = 64; // max = unlimited // maximum number of proofs (M account + N storage) // note: if this number is too small, protocol can be changed to uint16 maxUniqueProofs = 128; // max = 256 // maximum number of targets (accountProofs) maxUniqueTargets = 32; // max = maxUniqueProofs // maximum number of proofs per _getProof proofBatchSize = 16; // max = unlimited // maximum bytes from single readHashedBytes(), readFetchedBytes() // when readBytesAt() is not available maxSuppliedBytes = 13125 << 5; // max = unlimited, ~420KB @ 30m gas // maximum bytes from single read(), readBytes() maxProvableBytes = 64 << 5; // max = 32 * proof count // maximum bytes allocated by concat() and slice() maxAllocBytes = 1 << 20; // max = server memory // maximum recursion depth maxEvalDepth = 5; // max = unlimited // use getCode() / getStorage() if no proof is cached yet fast = true; // console.log OP_DEBUG statements printDebug = true; constructor(provider) { this.provider = provider; } [Symbol.for('nodejs.util.inspect.custom')]() { return `${this.constructor.name}[${Object.entries(this.context).map(([k, v]) => `${k}=${v}`)}]`; } checkProofCount(size) { if (size > this.maxUniqueProofs) { throw new Error(`too many proofs: ${size} > ${this.maxUniqueProofs}`); } } checkStorageProofs(isContract, slots, proofs) { if (isContract) { // 20241112: devcon bug with linea-sepolia rpc // apply rpc check to all provers const n = proofs.reduce((a, x) => a + (x ? 1 : 0), 0); if (n !== slots.length) { throw new Error(`expected ${slots.length} storage proofs: got ${n}`); } } else { proofs.length = 0; // nuke the proofs } } proofMap() { const map = new Map(); for (const key of this.proofLRU.keys()) { if (key.startsWith('0x')) { const target = key.slice(0, 42); let bucket = map.get(target); if (!bucket) { bucket = []; map.set(target, bucket); } if (key.length > 42) { bucket.push(BigInt('0x' + key.slice(42))); } } } return map; } async proveV1(needs) { requireV1Needs(needs); const { proofs, order } = await this.prove(needs); return { accountProof: proofs[order[0]], storageProofs: Array.from(order.subarray(1), (i) => proofs[i]), }; } // machine interface async evalDecoded(v) { return this.evalReader(reader_js_1.ProgramReader.fromBytes(v)); } async evalRequest(req) { return this.evalReader(reader_js_1.ProgramReader.fromProgram(req)); } async evalReader(reader) { const vm = GatewayVM.create(reader.readByte(), // number of outputs this.maxStackSize, this.maxAllocBytes); await this.eval(reader, vm, 0); return vm; } async eval(reader, vm, depth) { if (depth > this.maxEvalDepth) throw new Error('max eval depth'); while (reader.remaining) { const op = reader.readByte(); if (op <= 32) { vm.pushUint256(reader.readBytes(op)); continue; } switch (op) { case ops_js_1.GATEWAY_OP.SET_TARGET: { const target = addressFromHex(await (0, wrap_js_1.unwrap)(vm.pop())); // IDEA: this could incrementally build the needs map // instead of doing it during prove() let need = vm.targets.get(target); if (!need) { if (vm.targets.size >= this.maxUniqueTargets) { throw new Error('too many targets'); } // NOTE: changing the target doesn't necessarily include an account proof // an account proof is included, either: // 1.) 2-level trie (stateRoot => storageRoot => slot) // 2.) we need to prove it is a contract (non-null codehash) // (native balance and other account state is not currently supported) need = { target, required: false }; vm.targets.set(target, need); } vm.needs.push(need); vm.target = target; vm.slot = 0n; // slot is reset when target is changed continue; } case ops_js_1.GATEWAY_OP.FOLLOW: { vm.slot = solidityFollowSlot(vm.slot, await (0, wrap_js_1.unwrap)(vm.pop())); continue; } case ops_js_1.GATEWAY_OP.SET_SLOT: { vm.slot = uint256FromHex(await (0, wrap_js_1.unwrap)(vm.pop())); continue; } case ops_js_1.GATEWAY_OP.ADD_SLOT: { vm.slot += uint256FromHex(await (0, wrap_js_1.unwrap)(vm.pop())); continue; } case ops_js_1.GATEWAY_OP.SET_OUTPUT: { vm.outputs[vm.checkOutputIndex(await vm.popNumber())] = vm.pop(); continue; } case ops_js_1.GATEWAY_OP.PUSH_OUTPUT: { vm.push(vm.outputs[vm.checkOutputIndex(await vm.popNumber())]); continue; } case ops_js_1.GATEWAY_OP.PUSH_BYTES: { vm.push(reader.readBytes(Number(reader.readUint()))); continue; } case ops_js_1.GATEWAY_OP.GET_SLOT: { vm.pushUint256(vm.slot); // current slot register continue; } case ops_js_1.GATEWAY_OP.GET_TARGET: { vm.push(vm.target); // current target address continue; } case ops_js_1.GATEWAY_OP.STACK_SIZE: { vm.pushUint256(vm.stack.length); continue; } case ops_js_1.GATEWAY_OP.IS_CONTRACT: { const need = vm.targets.get(vm.target); if (need) need.required = true; vm.pushUint256(await this.isContract(vm.target)); continue; } case ops_js_1.GATEWAY_OP.PUSH_STACK: { vm.push(vm.stack[vm.checkStackIndex(await vm.popNumber())]); continue; } case ops_js_1.GATEWAY_OP.DUP: { vm.push(vm.stack[vm.checkBack(await vm.popNumber())]); continue; } case ops_js_1.GATEWAY_OP.POP: { vm.stack.pop(); continue; } case ops_js_1.GATEWAY_OP.SWAP: { const back = vm.checkBack(await vm.popNumber()); const last = vm.stack.length - 1; const temp = vm.stack[back]; vm.stack[back] = vm.stack[last]; vm.stack[last] = temp; continue; } case ops_js_1.GATEWAY_OP.READ_SLOT: { const { target, slot } = vm; vm.needs.push(slot); vm.push(new wrap_js_1.Wrapped(32, () => this.getStorage(target, slot))); continue; } case ops_js_1.GATEWAY_OP.READ_SLOTS: { const { target, slot } = vm; const count = await vm.popNumber(); const size = checkSize(count << 5, this.maxProvableBytes); const slots = bigintRange(slot, count); vm.needs.push(...slots); vm.push(new wrap_js_1.Wrapped(size, async () => (0, utils_1.concat)(await Promise.all(slots.map((x) => this.getStorage(target, x)))))); continue; } case ops_js_1.GATEWAY_OP.READ_BYTES: { const { target, slot } = vm; const { value, slots } = await this.getStorageBytes(target, slot); vm.needs.push(slot, ...slots); vm.push(value); continue; } case ops_js_1.GATEWAY_OP.READ_HASHED_BYTES: { const { target, slot } = vm; const hash = vm.pop(); const value = this.fetchUnprovenStorageBytes(target, slot); vm.needs.push({ hash, value }); vm.push(value); continue; } case ops_js_1.GATEWAY_OP.READ_ARRAY: { const step = await vm.popNumber(); if (!step) throw new Error('invalid element size'); const { target, slot } = vm; let length = checkSize(uint256FromHex(await this.getStorage(target, slot)), this.maxProvableBytes); if (step < 32) { const per = (32 / step) | 0; length = ((length + per - 1) / per) | 0; } else { length = length * ((step + 31) >> 5); } const size = checkSize(length << 5, this.maxProvableBytes); const slots = solidityArraySlots(slot, length); slots.unshift(slot); vm.needs.push(...slots); vm.push(new wrap_js_1.Wrapped(size, async () => (0, utils_1.concat)(await Promise.all(slots.map((x) => this.getStorage(target, x)))))); continue; } case ops_js_1.GATEWAY_OP.EVAL: { const [code, cond] = vm.popSlice(2); if (!isZeros(await (0, wrap_js_1.unwrap)(cond))) { const program = reader_js_1.ProgramReader.fromBytes(await (0, wrap_js_1.unwrap)(code)); await this.eval(program, vm, depth + 1); if (vm.exitCode) return; } continue; } case ops_js_1.GATEWAY_OP.EVAL_LOOP: { const flags = reader.readByte(); const [code, n] = await Promise.all(vm.popSlice(2).map(wrap_js_1.unwrap)); const program = reader_js_1.ProgramReader.fromBytes(code); const vm2 = new GatewayVM(vm.outputs, vm.maxStack, vm.allocBudget, vm.needs, vm.targets); let count = Math.min(numberFromHex(n), vm.stack.length); while (count) { --count; vm2.target = vm.target; vm2.slot = vm.slot; vm2.stack = [vm.pop()]; vm2.exitCode = 0; program.pos = 0; await this.eval(program, vm2, depth + 1); if (flags & (vm2.exitCode ? STOP_ON_FAILURE : STOP_ON_SUCCESS)) { if (~flags & KEEP_ARGS) { vm.popSlice(count); } if (flags & ACQUIRE_STATE) { vm.target = vm2.target; vm.slot = vm2.slot; vm.stack.push(...vm2.stack); } break; } } vm.allocBudget = vm2.allocBudget; continue; } case ops_js_1.GATEWAY_OP.ASSERT: { const code = reader.readByte(); if (isZeros(await (0, wrap_js_1.unwrap)(vm.pop()))) { vm.exitCode = code; return; } continue; } case ops_js_1.GATEWAY_OP.KECCAK: { vm.push((0, crypto_1.keccak256)(await (0, wrap_js_1.unwrap)(vm.pop()))); continue; } case ops_js_1.GATEWAY_OP.CONCAT: { const v = (0, utils_1.concat)(await Promise.all(vm.popSlice(2).map(wrap_js_1.unwrap))); vm.checkAlloc((v.length - 2) >> 1); vm.push(v); continue; } case ops_js_1.GATEWAY_OP.SLICE: { const [v, x, n] = await Promise.all(vm.popSlice(3).map(wrap_js_1.unwrap)); const pos = numberFromHex(x); const size = numberFromHex(n); vm.checkAlloc(size); const len = (v.length - 2) >> 1; const end = pos + size; if (len >= end) { vm.push((0, utils_1.dataSlice)(v, pos, end)); } else { const prefix = pos >= len ? '0x' : (0, utils_1.dataSlice)(v, pos, len); vm.push(prefix.padEnd((size + 1) << 1, '0')); } continue; } case ops_js_1.GATEWAY_OP.LENGTH: { vm.pushUint256(await peekSize(vm.pop())); continue; } case ops_js_1.GATEWAY_OP.PLUS: { await vm.binaryOp((a, b) => a + b); continue; } case ops_js_1.GATEWAY_OP.TIMES: { await vm.binaryOp((a, b) => a * b); continue; } case ops_js_1.GATEWAY_OP.DIVIDE: { await vm.binaryOp((a, b) => a / b); continue; } case ops_js_1.GATEWAY_OP.MOD: { await vm.binaryOp((a, b) => a % b); continue; } case ops_js_1.GATEWAY_OP.POW: { await vm.binaryOp(pow256); continue; } case ops_js_1.GATEWAY_OP.AND: { await vm.binaryOp((a, b) => a & b); continue; } case ops_js_1.GATEWAY_OP.OR: { await vm.binaryOp((a, b) => a | b); continue; } case ops_js_1.GATEWAY_OP.XOR: { await vm.binaryOp((a, b) => a ^ b); continue; } case ops_js_1.GATEWAY_OP.SHIFT_LEFT: { await vm.binaryOp((x, shift) => x << shift); continue; } case ops_js_1.GATEWAY_OP.SHIFT_RIGHT: { await vm.binaryOp((x, shift) => x >> shift); continue; } case ops_js_1.GATEWAY_OP.EQ: { await vm.binaryOp((a, b) => a == b); continue; } case ops_js_1.GATEWAY_OP.LT: { await vm.binaryOp((a, b) => a < b); continue; } case ops_js_1.GATEWAY_OP.GT: { await vm.binaryOp((a, b) => a > b); continue; } case ops_js_1.GATEWAY_OP.IS_ZERO: { vm.pushUint256(isZeros(await (0, wrap_js_1.unwrap)(vm.pop()))); continue; } case ops_js_1.GATEWAY_OP.NOT: { vm.pushUint256(~uint256FromHex(await (0, wrap_js_1.unwrap)(vm.pop()))); continue; } case ops_js_1.GATEWAY_OP.DEBUG: { const label = reader.readSmallStr(); if (this.printDebug) { // TODO: this could ask the prover for more information // eg. BlockProver => block w/ stateRoot // this could also include vm.storageRoot const [stack, outputs] = await Promise.all([ vm.resolveStack(), vm.resolveOutputs(), ]); console.log(`DEBUG(${(0, ezccip_1.asciiize)(label)})`, { target: vm.target, slot: vm.slot, exitCode: vm.exitCode, stack, outputs, needs: vm.needs, }); } continue; } default: { throw new Error(`unknown op: ${op}`); } } } } fetchUnprovenStorageBytes(target, slot) { target = target.toLowerCase(); return new wrap_js_1.Wrapped(NaN, async () => { const can = this.readBytesAtSupported.get(target); if (can !== false) { try { const contract = new contract_1.Contract(target, GATEWAY_EXT_ABI, this.provider); const v = await contract.readBytesAt(slot); if (!can) this.readBytesAtSupported.set(target, true); return v; } catch (err) { if (!can && (0, utils_js_1.isRevert)(err)) this.readBytesAtSupported.set(target, false); } } const { value } = await this.getStorageBytes(target, slot, true); return (0, wrap_js_1.unwrap)(value); }); } async getStorageBytes(target, slot, fast) { // https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#bytes-and-string const first = await this.getStorage(target, slot, fast); let size = parseInt(first.slice(64), 16); // last byte if ((size & 1) == 0) { // small size >>= 1; const value = (0, utils_1.dataSlice)(first, 0, size); // will throw if size is invalid return { value, size, slots: [] }; } size = checkSize(BigInt(first) >> 1n, fast ? this.maxSuppliedBytes : this.maxProvableBytes); if (size < 32) { throw new Error(`invalid storage encoding: ${target} @ ${slot}`); } const slots = solidityArraySlots(slot, (size + 31) >> 5); const value = new wrap_js_1.Wrapped(size, async () => { const v = await Promise.all(slots.map((x) => this.getStorage(target, x, fast))); return (0, utils_1.dataSlice)((0, utils_1.concat)(v), 0, size); }); return { value, size, slots }; } } exports.AbstractProver = AbstractProver; class BlockProver extends AbstractProver { static _createLatest() { return async (provider, relBlockTag = utils_js_1.LATEST_BLOCK_TAG) => { return new this(provider, await (0, utils_js_1.fetchBlockNumber)(provider, relBlockTag)); }; } block; constructor(provider, block) { super(provider); this.block = (0, utils_js_1.toUnpaddedHex)(block); } get context() { return { block: this.blockNumber }; } get blockNumber() { return BigInt(this.block); } fetchBlock() { return this.cache.get('BLOCK', () => (0, utils_js_1.fetchBlock)(this.provider, this.block)); } async fetchStateRoot() { return (await this.fetchBlock()).stateRoot; } async fetchTimestamp() { return parseInt((await this.fetchBlock()).timestamp); } async prove(needs) { const promises = []; const buckets = new Map(); const refs = []; let nullRef; const createRef = () => { const ref = { id: refs.length, proof: '0x' }; refs.push(ref); return ref; }; let bucket; const order = needs.map((need) => { if (isTargetNeed(need)) { // accountProof // we must prove this value since it leads to a stateRoot bucket = buckets.get(need.target); if (!bucket) { bucket = { need, ref: createRef(), map: new Map(), }; buckets.set(need.target, bucket); } return bucket.ref; } else if (typeof need === 'bigint') { // storageProof (for targeted account) // bucket can be undefined if a slot is read without a target // this is okay because the initial machine state is NOT_A_CONTRACT if (!bucket) return (nullRef ??= createRef()); let ref = bucket.map.get(need); if (!ref) { ref = createRef(); bucket.map.set(need, ref); } return ref; } else { // currently, this is just HashedNeed // TODO: check the hash? const ref = createRef(); promises.push((async () => (ref.proof = await (0, wrap_js_1.unwrap)(need.value)))()); return ref; } }); this.checkProofCount(refs.length); for (const bucket of buckets.values()) { // NOTE: technically, we only need to prove the account // if the state was accessed or storage was read // because we can set an invalid storageRoot // but this is rare and makes machine complicated // as it requires 3 states: proven true, proven false, unknown // so far, only ZKSync has this functionality due to gas: // see: ZKSyncHookVerifierHooks.sol:verifyAccountState() // see: ZKSyncProver.ts:prove() //if (bucket.need.required || bucket.map.size) { promises.push(this._proveNeed(bucket.need, bucket.ref, bucket.map)); } await Promise.all(promises); return { proofs: refs.map((x) => x.proof), order: Uint8Array.from(order, (x) => x.id), }; } } exports.BlockProver = BlockProver;