UNPKG

@unruggable/gateways

Version:

Trustless Ethereum Multichain CCIP-Read Gateway

179 lines (178 loc) 8.12 kB
import { supportsV1 } from './rollup.mjs'; import { Interface } from 'ethers/abi'; import { solidityPackedKeccak256, id as keccakStr } from 'ethers/hash'; import { getBytes } from 'ethers/utils'; import { CachedMap, CachedValue, LRU } from './cached.mjs'; import { ABI_CODER } from './utils.mjs'; import { GatewayRequestV1 } from './v1.mjs'; import { EZCCIP } from '@namestone/ezccip'; export const GATEWAY_ABI = new 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; export class Gateway extends 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 CachedValue(() => this.rollup.fetchLatestCommitIndex(), pollMs0); commitCacheMap = new CachedMap(Infinity); callLRU = new LRU(callCapacity0); constructor(rollup) { super(); this.rollup = rollup; this.register(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 = 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 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 (supportsV1(rollup)) { this.register(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 = keccakStr(`${commit.index}:${context.calldata}`); history.show = [commit.index, shortHash(hash)]; return this.callLRU.cache(hash, async () => { const req = new 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 getBytes(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 CachedValue(() => this.rollup.fetchParentCommitIndex(commit), this.latestCache.cacheMs); const valid = new 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; } } export class GatewayV1 extends EZCCIP { rollup; latestCommit; latestCache = new 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 LRU(callCapacity0); constructor(rollup) { super(); this.rollup = rollup; this.register(GATEWAY_ABI, { getStorageSlots: async ([target, commands, constants], context, history) => { const commit = await this.getLatestCommit(); const hash = keccakStr(`${commit.index}:${context.calldata}`); history.show = [commit.index, shortHash(hash)]; return this.callLRU.cache(hash, async () => { const req = new GatewayRequestV1(target, commands, constants); return getBytes(await this.handleRequest(commit, req)); }); }, }); } getLatestCommit() { return this.latestCache.get(); } disableCache() { this.latestCache.cacheMs = 0; this.callLRU.max = 0; } }