@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
188 lines (187 loc) • 7.51 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZKSyncProver = exports.ZKSYNC_ACCOUNT_CODEHASH = void 0;
const types_js_1 = require("./types.cjs");
const vm_js_1 = require("../vm.cjs");
const constants_1 = require("ethers/constants");
const utils_js_1 = require("../utils.cjs");
const wrap_js_1 = require("../wrap.cjs");
// https://docs.zksync.io/build/api-reference/zks-rpc#zks_getproof
// https://github.com/matter-labs/era-contracts/blob/fd4aebcfe8833b26e096e87e142a5e7e4744f3fa/system-contracts/bootloader/bootloader.yul#L458
exports.ZKSYNC_ACCOUNT_CODEHASH = '0x0000000000000000000000000000000000008002';
// zksync proofs are relative to a *batch* not a *block*
class ZKSyncProver extends vm_js_1.AbstractProver {
batchIndex;
static encodeProof = types_js_1.encodeProof;
static async latestBatchIndex(provider, relBlockTag = utils_js_1.LATEST_BLOCK_TAG) {
// https://docs.zksync.io/build/api-reference/zks-rpc#zks_l1batchnumber
// NOTE: BlockTags are not supported
// we could simulate "finalized" using some fixed offset
// currently: any block tag => "latest"
if ((0, utils_js_1.isBlockTag)(relBlockTag)) {
relBlockTag = 0;
}
else {
relBlockTag = Number(relBlockTag);
if (relBlockTag >= 0)
return relBlockTag;
}
const batchIndex = Number(await provider.send('zks_L1BatchNumber', []));
return batchIndex + relBlockTag;
}
static async latest(provider, relBlockTag = utils_js_1.LATEST_BLOCK_TAG) {
return new this(provider, await this.latestBatchIndex(provider, relBlockTag));
}
constructor(provider, batchIndex) {
super(provider);
this.batchIndex = batchIndex;
}
get context() {
return { batch: this.batchIndex };
}
fetchBatchDetails() {
return this.cache.get('BATCH', async () => {
// https://docs.zksync.io/build/api-reference/zks-rpc#zks_getl1batchdetails
const json = await this.provider.send('zks_getL1BatchDetails', [
this.batchIndex,
]);
if (!json)
throw new Error(`no batch: ${this.batchIndex}`);
if (!json.rootHash)
throw new Error(`unprovable batch: ${this.batchIndex}`);
return json;
});
}
async fetchStateRoot() {
return (await this.fetchBatchDetails()).rootHash;
}
async fetchTimestamp() {
return (await this.fetchBatchDetails()).timestamp;
}
async isContract(target) {
const storageProof = await this.proofLRU.touch(target.toLowerCase());
const codeHash = storageProof
? storageProof.value
: await this.getStorage(exports.ZKSYNC_ACCOUNT_CODEHASH, BigInt(target));
return !/^0x0+$/.test(codeHash);
}
async getStorage(target, slot
//_fast: boolean = this.fast
) {
target = target.toLowerCase();
const storageKey = (0, vm_js_1.makeStorageKey)(target, slot);
const storageProof = await this.proofLRU.touch(storageKey);
if (storageProof) {
return storageProof.value;
}
// 20240407: requires efficient batch => block
// if (fast) {
// return this.cache.get(storageKey, () => {
// return fetchStorage(this.provider, target, slot, this.batchIndex);
// });
// }
const vs = await this.getStorageProofs(target, [slot]);
return vs.length ? (0, utils_js_1.toPaddedHex)(vs[0].value) : constants_1.ZeroHash;
}
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;
};
const addSlot = (target, slot) => {
if (target === constants_1.ZeroAddress)
return (nullRef ??= createRef());
let bucket = buckets.get(target);
if (!bucket) {
bucket = new Map();
buckets.set(target, bucket);
}
let ref = bucket.get(slot);
if (!ref) {
ref = createRef();
bucket.set(slot, ref);
}
return ref;
};
let target = constants_1.ZeroAddress;
const order = needs.map((need) => {
if ((0, vm_js_1.isTargetNeed)(need)) {
// codehash for contract A is not stored in A
// it is stored in the global codehash contract
target = need.target;
return addSlot(need.required ? exports.ZKSYNC_ACCOUNT_CODEHASH : constants_1.ZeroAddress, BigInt(need.target));
}
else if (typeof need === 'bigint') {
return addSlot(target, need);
}
else {
const ref = createRef();
promises.push((async () => {
ref.proof = await (0, wrap_js_1.unwrap)(need.value);
})());
return ref;
}
});
this.checkProofCount(refs.length);
await Promise.all(promises.concat(Array.from(buckets, async ([target, map]) => {
const m = [...map];
const proofs = await this.getStorageProofs(target, m.map(([slot]) => slot));
m.forEach(([, ref], i) => (ref.proof = (0, types_js_1.encodeProof)(proofs[i])));
})));
return {
proofs: refs.map((x) => x.proof),
order: Uint8Array.from(order, (x) => x.id),
};
}
async getStorageProofs(target, slots) {
target = target.toLowerCase();
const missing = [];
const { promise, resolve, reject } = (0, utils_js_1.withResolvers)();
const storageProofs = slots.map((slot, i) => {
const key = (0, vm_js_1.makeStorageKey)(target, slot);
const p = this.proofLRU.touch(key);
if (!p) {
this.proofLRU.setFuture(key, promise.then(() => storageProofs[i]));
missing.push(i);
}
return p;
});
if (missing.length) {
try {
const vs = await this.fetchStorageProofs(target, missing.map((x) => slots[x]));
missing.forEach((x, i) => (storageProofs[x] = vs[i]));
resolve();
}
catch (err) {
reject(err);
throw err;
}
}
// 20241112: assume that the rpc is correct
// any missing storage proof implies the account is not a contract
// otherwise, we need another proof to perform this check
const v = await Promise.all(storageProofs);
this.checkStorageProofs(v.every((x) => x), slots, v);
return v;
}
async fetchStorageProofs(target, slots) {
const ps = [];
for (let i = 0; i < slots.length;) {
ps.push(this.provider.send('zks_getProof', [
target,
slots
.slice(i, (i += this.proofBatchSize))
.map((slot) => (0, utils_js_1.toPaddedHex)(slot)),
this.batchIndex,
]));
}
const vs = await Promise.all(ps);
return vs.flatMap((x) => x.storageProof);
}
}
exports.ZKSyncProver = ZKSyncProver;