UNPKG

@unruggable/gateways

Version:

Trustless Ethereum Multichain CCIP-Read Gateway

184 lines (183 loc) 7.19 kB
import { encodeProof, } from './types.mjs'; import { AbstractProver, isTargetNeed, makeStorageKey, } from '../vm.mjs'; import { ZeroAddress, ZeroHash } from 'ethers/constants'; import { LATEST_BLOCK_TAG, isBlockTag, toPaddedHex, withResolvers, } from '../utils.mjs'; import { unwrap } from '../wrap.mjs'; // 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 export const ZKSYNC_ACCOUNT_CODEHASH = '0x0000000000000000000000000000000000008002'; // zksync proofs are relative to a *batch* not a *block* export class ZKSyncProver extends AbstractProver { batchIndex; static encodeProof = encodeProof; static async latestBatchIndex(provider, relBlockTag = 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 (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 = 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(ZKSYNC_ACCOUNT_CODEHASH, BigInt(target)); return !/^0x0+$/.test(codeHash); } async getStorage(target, slot //_fast: boolean = this.fast ) { target = target.toLowerCase(); const storageKey = 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 ? toPaddedHex(vs[0].value) : 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 === 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 = ZeroAddress; const order = needs.map((need) => { if (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 ? ZKSYNC_ACCOUNT_CODEHASH : ZeroAddress, BigInt(need.target)); } else if (typeof need === 'bigint') { return addSlot(target, need); } else { const ref = createRef(); promises.push((async () => { ref.proof = await 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 = 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 } = withResolvers(); const storageProofs = slots.map((slot, i) => { const key = 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) => toPaddedHex(slot)), this.batchIndex, ])); } const vs = await Promise.all(ps); return vs.flatMap((x) => x.storageProof); } }