UNPKG

@unruggable/gateways

Version:

Trustless Ethereum Multichain CCIP-Read Gateway

184 lines (183 loc) 8.48 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GatewayV1 = exports.Gateway = exports.GATEWAY_ABI = void 0; const rollup_js_1 = require("./rollup.cjs"); const abi_1 = require("ethers/abi"); const hash_1 = require("ethers/hash"); const utils_1 = require("ethers/utils"); const cached_js_1 = require("./cached.cjs"); const utils_js_1 = require("./utils.cjs"); const v1_js_1 = require("./v1.cjs"); const ezccip_1 = require("@namestone/ezccip"); exports.GATEWAY_ABI = new abi_1.Interface([ `function proveRequest(bytes context, tuple(bytes)) returns (bytes)`, // `function timestamp() returns (uint256)`, // V1 `function getStorageSlots(address target, bytes32[] commands, bytes[] constants) returns (bytes)`, ]); // shorten the request hash for less spam and easier comparision function shortHash(x) { return x.slice(-8); } // default number of answers to keep in memory // (what is the distribution of real-load resolution counts?) // edit with: gateway.callLRU.max const callCapacity0 = 1000; // ms to wait until checking for a new commit // edit with: gateway.latestCache.cacheMs const pollMs0 = 60000; class Gateway extends ezccip_1.EZCCIP { rollup; // the max number of non-latest commitments to keep in memory commitDepth = 1; // if true, requests beyond the commit depth are supported allowHistorical = false; latestCache = new cached_js_1.CachedValue(() => this.rollup.fetchLatestCommitIndex(), pollMs0); commitCacheMap = new cached_js_1.CachedMap(Infinity); callLRU = new cached_js_1.LRU(callCapacity0); constructor(rollup) { super(); this.rollup = rollup; this.register(exports.GATEWAY_ABI, { proveRequest: async ([ctx, [req]], _context, history) => { // given the requested commitment, we answer: min(requested, latest) const commit = await this.getRecentCommit(BigInt(ctx.slice(0, 66))); // we cannot hash the context.calldata directly because the requested // commit might be different, so we hash using the determined commit const hash = (0, hash_1.solidityPackedKeccak256)(['uint256', 'bytes'], [commit.index, req]); history.show = [commit.index, shortHash(hash)]; // NOTE: for a given commit + request, calls are pure return this.callLRU.cache(hash, async () => { const state = await commit.prover.evalDecoded(req); const proofSeq = await commit.prover.prove(state.needs); return (0, utils_1.getBytes)(this.rollup.encodeWitness(commit, proofSeq)); }); }, // timestamp: async () => { // const commit = await this.getLatestCommit(); // return [await commit.prover.fetchTimestamp()]; // }, }); // NOTE: this only works if V1 and V2 share same proof encoding! if ((0, rollup_js_1.supportsV1)(rollup)) { this.register(exports.GATEWAY_ABI, { getStorageSlots: async ([target, commands, constants], context, history) => { // V1 protocol is always latest const commit = await this.getLatestCommit(); // we cannot hash the context.calldata directly because the request // doesn't contain the specific commit index const hash = (0, hash_1.id)(`${commit.index}:${context.calldata}`); history.show = [commit.index, shortHash(hash)]; return this.callLRU.cache(hash, async () => { const req = new v1_js_1.GatewayRequestV1(target, commands, constants).v2(); // upgrade v1 to v2 const state = await commit.prover.evalRequest(req); const proofSeq = await commit.prover.proveV1(state.needs); const witness = rollup.encodeWitnessV1(commit, proofSeq); return (0, utils_1.getBytes)(utils_js_1.ABI_CODER.encode(['bytes'], [witness])); }); }, }); } } async _updateLatest() { const prev = await this.latestCache.value?.catch(() => { }); const next = await this.latestCache.get(); const cached = await this.cachedCommit(next); const max = this.commitDepth + 1; // depth + latest if (prev !== next && this.commitCacheMap.cachedSize > max) { // purge the oldest if we have too many // note: this will nuke any historicals const old = [...this.commitCacheMap.cachedKeys()].sort().slice(0, -max); for (const key of old) { this.commitCacheMap.delete(key); } } return cached; } async getLatestCommit() { return (await this._updateLatest()).commit; } async getParentCommit(commit) { const cached = await this.cachedCommit(commit.index); const parent = await this.cachedCommit(await cached.parent.get()); return parent.commit; } async getRecentCommit(index) { const latest = await this._updateLatest(); let cursor = latest; // check recent cache in linear order for (let depth = 0;;) { if (index >= cursor.commit.index) return cursor.commit; if (++depth >= this.commitDepth) break; const prev = await cursor.parent.get(); cursor = await this.cachedCommit(prev); } // if older than that, consider one-off commit // this can be unaligned but must be finalized if (this.allowHistorical) { // 20240926: maybe this should be cached for a bit (was 0) return (await this.cachedCommit(index, 250)).commit; } throw new Error(`too old: ${index} vs ${latest.commit.index}[depth=${this.commitDepth}]`); } async cachedCommit(index, cacheMs) { const cached = await this.commitCacheMap.peek(index); if (cached && !(await cached.valid.get().catch(() => { }))) { this.commitCacheMap.delete(index); } return this.commitCacheMap.get(index, async (i) => { const commit = await this.rollup.fetchCommit(i); const parent = new cached_js_1.CachedValue(() => this.rollup.fetchParentCommitIndex(commit), this.latestCache.cacheMs); const valid = new cached_js_1.CachedValue(() => this.rollup.isCommitStillValid(commit), this.latestCache.cacheMs); valid.set(true); // mark it as valid return { commit, valid, parent }; }, cacheMs); } disableCache() { this.latestCache.cacheMs = 0; this.commitCacheMap.cacheMs = 0; this.callLRU.max = 0; } } exports.Gateway = Gateway; class GatewayV1 extends ezccip_1.EZCCIP { rollup; latestCommit; latestCache = new cached_js_1.CachedValue(async () => { const index = await this.rollup.fetchLatestCommitIndex(); // since we can only serve the latest commit // we only keep the latest commit if (!this.latestCache.cacheMs || index !== this.latestCommit?.index || !(await this.rollup.isCommitStillValid(this.latestCommit).catch(() => { }))) { this.latestCommit = await this.rollup.fetchCommit(index); } return this.latestCommit; }, pollMs0); callLRU = new cached_js_1.LRU(callCapacity0); constructor(rollup) { super(); this.rollup = rollup; this.register(exports.GATEWAY_ABI, { getStorageSlots: async ([target, commands, constants], context, history) => { const commit = await this.getLatestCommit(); const hash = (0, hash_1.id)(`${commit.index}:${context.calldata}`); history.show = [commit.index, shortHash(hash)]; return this.callLRU.cache(hash, async () => { const req = new v1_js_1.GatewayRequestV1(target, commands, constants); return (0, utils_1.getBytes)(await this.handleRequest(commit, req)); }); }, }); } getLatestCommit() { return this.latestCache.get(); } disableCache() { this.latestCache.cacheMs = 0; this.callLRU.max = 0; } } exports.GatewayV1 = GatewayV1;