@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
184 lines (183 loc) • 8.48 kB
JavaScript
"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;