@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
122 lines (121 loc) • 4.93 kB
JavaScript
import { isContract, encodeProof, } from './types.mjs';
import { BlockProver, makeStorageKey } from '../vm.mjs';
import { ZeroHash } from 'ethers/constants';
import { withResolvers, toPaddedHex, fetchStorage } from '../utils.mjs';
export class EthProver extends BlockProver {
static encodeProof = encodeProof;
static isContract = isContract;
static latest = this._createLatest();
async isContract(target) {
target = target.toLowerCase();
if (this.fast) {
return this.cache.get(target, async (a) => {
// note: this actually reverts when the block is bad
// eg. {"code": -32602, "message": "Unknown block number"}
const code = await this.provider.getCode(a, this.block);
return code.length > 2;
});
}
return isContract(await this.getProofs(target));
}
async getStorage(target, slot, fast = this.fast) {
target = target.toLowerCase();
// check to see if we know this target isn't a contract without invoking provider
// this is almost equivalent to: await isContract(target)
const accountProof = await this.proofLRU.touch(target);
if (accountProof && !isContract(accountProof)) {
return ZeroHash;
}
// check to see if we've already have a proof for this value
const storageKey = makeStorageKey(target, slot);
const storageProof = await this.proofLRU.touch(storageKey);
if (storageProof) {
return toPaddedHex(storageProof.value);
}
if (fast) {
return this.cache.get(storageKey, () => {
return fetchStorage(this.provider, target, slot, this.block);
});
}
const proofs = await this.getProofs(target, [slot]);
return isContract(proofs)
? toPaddedHex(proofs.storageProof[0].value)
: ZeroHash;
}
async _proveNeed(need, accountRef, slotRefs) {
const m = [...slotRefs];
const accountProof = await this.proofLRU.peek(need.target);
if (accountProof && !isContract(accountProof))
m.length = 0;
const proofs = await this.getProofs(need.target, m.map(([slot]) => slot));
accountRef.proof = encodeProof(proofs.accountProof);
if (isContract(proofs)) {
m.forEach(([, ref], i) => (ref.proof = encodeProof(proofs.storageProof[i].proof)));
}
}
async getProofs(target, slots = []) {
target = target.toLowerCase();
const missing = []; // indices of slots we don't have proofs for
const { promise, resolve, reject } = withResolvers(); // create a blocker
// 20240708: must setup blocks before await
let accountProof = this.proofLRU.touch(target);
if (!accountProof) {
// missing account proof, so block it
this.proofLRU.setFuture(target, promise.then(() => accountProof));
}
// check if we're missing any slots
const storageProofs = slots.map((slot, i) => {
const key = makeStorageKey(target, slot);
const p = this.proofLRU.touch(key);
if (!p) {
// missing storage proof, so block it
this.proofLRU.setFuture(key, promise.then(() => storageProofs[i]));
missing.push(i);
}
return p;
});
// check if we need something
if (!accountProof || missing.length) {
try {
const { storageProof: v, ...a } = await this.fetchProofs(target, missing.map((x) => slots[x]));
// update cache
accountProof = a;
missing.forEach((x, i) => (storageProofs[x] = v[i]));
resolve(); // unblock
}
catch (err) {
reject(err);
throw err;
}
}
// reassemble
const [a, v] = await Promise.all([
accountProof,
Promise.all(storageProofs),
]);
this.checkStorageProofs(isContract(a), slots, v);
return { storageProof: v, ...a };
}
async fetchProofs(target, slots = []) {
const ps = [];
for (let i = 0;;) {
ps.push(this.provider.send('eth_getProof', [
target,
slots
.slice(i, (i += this.proofBatchSize))
.map((slot) => toPaddedHex(slot)),
this.block,
]));
if (i >= slots.length)
break;
}
const vs = await Promise.all(ps);
// note: this returns null when block is bad
if (!vs[0])
throw new Error(`unprovable block: ${this.block}`);
for (let i = 1; i < vs.length; i++) {
vs[0].storageProof.push(...vs[i].storageProof);
}
return vs[0];
}
}