@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
200 lines (199 loc) • 8.12 kB
JavaScript
import { AbstractRollup, } from '../rollup.mjs';
import { ROOT_CHAIN_ABI } from './types.mjs';
import { ZeroHash } from 'ethers/constants';
import { Contract } from 'ethers/contract';
import { id as keccakStr } from 'ethers/hash';
import { CHAINS } from '../chains.mjs';
import { EthProver } from '../eth/EthProver.mjs';
import { ABI_CODER, fetchBlockFromHash, toUnpaddedHex } from '../utils.mjs';
import { encodeRlpBlock } from '../rlp.mjs';
export class PolygonPoSRollup extends AbstractRollup {
// https://docs.polygon.technology/pos/reference/contracts/genesis-contracts/
static mainnetConfig = {
chain1: CHAINS.MAINNET,
chain2: CHAINS.POLYGON_POS,
RootChain: '0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287',
apiURL: 'https://proof-generator.polygon.technology/api/v1/matic/',
poster: {
// https://polygonscan.com/tx/0x092f9929973fee6a4fa101e9ed45c2b6ce072ac6e2f338f49cac70b41cacbc73
address: '0x591663413423Dcf7c7806930E642951E0dDdf10B',
blockNumberStart: 61150865n,
topicHash: keccakStr('NewRoot(bytes32)'),
},
};
apiURL;
RootChain;
poster;
constructor(providers, config) {
super(providers);
this.apiURL = config.apiURL;
this.poster = config.poster;
this.RootChain = new Contract(config.RootChain, ROOT_CHAIN_ABI, this.provider1);
}
async findPosterEventBefore(l2BlockNumber) {
// find the most recent post from poster
// stop searching when earlier than poster deployment
// (otherwise we scan back to genesis)
const step = BigInt(this.getLogsStepSize);
for (let i = l2BlockNumber; i > this.poster.blockNumberStart; i -= step) {
const logs = await this.provider2.getLogs({
address: this.poster.address,
topics: [this.poster.topicHash],
fromBlock: i < step ? 0n : i - step,
toBlock: i - 1n,
});
if (logs.length)
return logs[logs.length - 1];
}
throw new Error(`no earlier root: ${l2BlockNumber}`);
}
async findPosterHeaderBefore(l2BlockNumber) {
// find the most recent post that occurred before this block
const event = await this.findPosterEventBefore(l2BlockNumber);
// find the header that contained this transaction
// 20240830: we want the header for the transaction
// not the header containing the logged block hash
return this.fetchAPIFindHeader(BigInt(event.blockNumber));
}
async fetchJSON(url) {
const res = await fetch(url);
if (!res.ok)
throw new Error(`${res.url}: HTTP(${res.status})`);
return res.json();
}
async fetchAPIFindHeader(l2BlockNumber) {
const url = new URL(`./block-included/${l2BlockNumber}`, this.apiURL);
const json = await this.fetchJSON(url);
if (json.error)
throw new Error(`Block(${l2BlockNumber}): ${json.message}`);
const number = BigInt(json.headerBlockNumber);
const l2BlockNumberStart = BigInt(json.start);
const l2BlockNumberEnd = BigInt(json.end);
const rootHash = json.root;
return {
number,
l2BlockNumberStart,
l2BlockNumberEnd,
rootHash,
};
}
// async fetchAPIHeaderProof(
// l2BlockNumber: bigint,
// l2BlockNumberStart: bigint,
// l2BlockNumberEnd: bigint
// ) {
// const url = new URL(`./fast-merkle-proof`, this.apiURL);
// url.searchParams.set('start', l2BlockNumberStart.toString());
// url.searchParams.set('end', l2BlockNumberEnd.toString());
// url.searchParams.set('number', l2BlockNumber.toString());
// const json = await this.fetchJSON(url);
// const v = ethers.getBytes(json.proof);
// if (!v.length || v.length & 31) throw new Error('expected bytes32xN');
// return Array.from({ length: v.length >> 5 }, (_, i) =>
// v.subarray(i << 5, (i + 1) << 5)
// );
// }
async fetchAPIReceiptProof(txHash) {
const url = new URL(`./exit-payload/${txHash}?eventSignature=${this.poster.topicHash}`, this.apiURL);
const json = await this.fetchJSON(url);
if (json.error)
throw new Error(`receipt proof: ${json.message}`);
return json.result;
}
async fetchLatestCommitIndex() {
// find the end range of the last header
const l2BlockNumberEnd = await this.RootChain.getLastChildBlock({
blockTag: this.latestBlockTag,
});
// find the header before the end of the last header with a post
const header = await this.findPosterHeaderBefore(l2BlockNumberEnd + 1n);
return header.number;
}
async _fetchParentCommitIndex(commit) {
const header = await this.findPosterHeaderBefore(commit.l2BlockNumberStart);
return header.number;
}
async _fetchCommit(index) {
// ensure checkpoint was finalized
const { rootHash, l2BlockNumberStart, l2BlockNumberEnd } = await this.RootChain.headerBlocks(index);
if (rootHash === ZeroHash) {
throw new Error(`null checkpoint hash`);
}
// ensure checkpoint contains post
const events = await this.provider2.getLogs({
address: this.poster.address,
topics: [this.poster.topicHash],
fromBlock: l2BlockNumberStart,
toBlock: l2BlockNumberEnd,
});
if (!events.length)
throw new Error('no poster');
const event = events[events.length - 1];
const prevBlockHash = event.topics[1];
// rlpEncodedProof:
// 1. checkpoint index
// 2. fast-merkle-proof => block in checkpoint
// 3. receipt merkle patricia proof => tx in block
// 4. receipt data: topic[1] w/prevBlockHash + logIndex
// rlpEncodedBlock:
// 5. hash() = prevBlockHash
// 6. usable stateRoot!
const [rlpEncodedProof, prevBlock] = await Promise.all([
this.fetchAPIReceiptProof(event.transactionHash),
fetchBlockFromHash(this.provider2, prevBlockHash),
]);
const rlpEncodedBlock = encodeRlpBlock(prevBlock);
// if (ethers.keccak256(rlpEncodedBlock) !== prevBlockHash) {
// throw new Error('block hash mismatch`);
// }
const prover = new EthProver(this.provider2, prevBlock.number);
return {
index,
prover,
rootHash,
l2BlockNumberStart,
l2BlockNumberEnd,
rlpEncodedProof,
rlpEncodedBlock,
};
}
encodeWitness(commit, proofSeq) {
return ABI_CODER.encode(['(bytes, bytes, bytes[], bytes)'], [
[
commit.rlpEncodedProof,
commit.rlpEncodedBlock,
proofSeq.proofs,
proofSeq.order,
],
]);
}
windowFromSec(sec) {
// finalization time is on-chain
return sec;
}
// experimental idea: commit serialization
JSONFromCommit(commit) {
return {
index: toUnpaddedHex(commit.index),
l2BlockNumber: commit.prover.block,
l2BlockNumberStart: toUnpaddedHex(commit.l2BlockNumberStart),
l2BlockNumberEnd: toUnpaddedHex(commit.l2BlockNumberEnd),
rlpEncodedBlock: commit.rlpEncodedBlock,
rlpEncodedProof: commit.rlpEncodedProof,
rootHash: commit.rootHash,
};
}
commitFromJSON(json) {
const commit = {
index: BigInt(json.index),
prover: new EthProver(this.provider2, json.l2BlockNumber),
l2BlockNumberStart: BigInt(json.l2BlockNumberStart),
l2BlockNumberEnd: BigInt(json.l2BlockNumberEnd),
rlpEncodedProof: json.rlpEncodedProof,
rlpEncodedBlock: json.rlpEncodedBlock,
rootHash: json.rootHash,
};
this.configure?.(commit);
return commit;
}
}