@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
1,012 lines (1,011 loc) • 37.6 kB
JavaScript
"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;