UNPKG

@unruggable/gateways

Version:

Trustless Ethereum Multichain CCIP-Read Gateway

197 lines (196 loc) 8.29 kB
import { AbstractRollup, } from '../rollup.mjs'; import { Contract, EventLog } from 'ethers/contract'; import { Interface } from 'ethers/abi'; import { keccak256 } from 'ethers/crypto'; import { concat, getBytes } from 'ethers/utils'; import { CHAINS } from '../chains.mjs'; import { EthProver } from '../eth/EthProver.mjs'; import { ABI_CODER, fetchBlock, toPaddedHex } from '../utils.mjs'; import { CachedValue } from '../cached.mjs'; import { fetchBeaconData, fetchSidecars } from '../beacon.mjs'; import { decompress } from 'fzstd'; // https://github.com/scroll-tech/go-ethereum/tree/24757865c6bbd9becb0256e97e8492d1f73987d9 // https://github.com/scroll-tech/scroll-contracts/blob/8e6a02b120d3a997f7c8e948b62bfb0e5b3ac185/src/L1/rollup/IScrollChain.sol const ROLLUP_ABI = new Interface([ `function lastFinalizedBatchIndex() view returns (uint256)`, `function finalizedStateRoots(uint256 batchIndex) view returns (bytes32)`, `event CommitBatch( uint256 indexed batchIndex, bytes32 indexed batchHash )`, `event FinalizeBatch( uint256 indexed batchIndex, bytes32 indexed batchHash, bytes32 stateRoot, bytes32 withdrawRoot )`, `function commitBatches( uint8 version, bytes32 parentBatchHash, bytes32 lastBatchHash, )`, `function commitAndFinalizeBatch( uint8 version, bytes32 parentBatchHash, ( bytes batchHeader, uint256 totalL1MessagesPoppedOverall, bytes32 postStateRoot, bytes32 withdrawRoot, bytes zkProof ) finalizeStruct )`, ]); const BATCH_VERSION = 7; export class EuclidRollup extends AbstractRollup { beaconAPI; // https://etherscan.io/address/0xa13BAF47339d63B743e7Da8741db5456DAc1E556 static mainnetConfig = { chain1: CHAINS.MAINNET, chain2: CHAINS.SCROLL, ScrollChain: '0xa13BAF47339d63B743e7Da8741db5456DAc1E556', }; // https://sepolia.etherscan.io/address/0x2D567EcE699Eabe5afCd141eDB7A4f2D0D6ce8a0 static sepoliaConfig = { chain1: CHAINS.SEPOLIA, chain2: CHAINS.SCROLL_SEPOLIA, ScrollChain: '0x2D567EcE699Eabe5afCd141eDB7A4f2D0D6ce8a0', }; ScrollChain; beaconConfig = new CachedValue(async () => { const [genesis, spec] = await Promise.all([ `${this.beaconAPI}/eth/v1/beacon/genesis`, `${this.beaconAPI}/eth/v1/config/spec`, ].map(fetchBeaconData)); return { genesisTime: BigInt(genesis.genesis_time), secondsPerSlot: BigInt(spec.SECONDS_PER_SLOT), }; }, Infinity); constructor(providers, config, beaconAPI) { super(providers); this.beaconAPI = beaconAPI; this.ScrollChain = new Contract(config.ScrollChain, ROLLUP_ABI, this.provider1); } async fetchLatestCommitIndex() { return this.ScrollChain.lastFinalizedBatchIndex({ blockTag: this.latestBlockTag, }); } async _fetchParentCommitIndex(commit) { return this.ScrollChain.lastFinalizedBatchIndex({ blockTag: commit.l1BlockNumber - 1, }); } async _fetchCommit(index) { // const [commitEvent] = await this.ScrollChain.queryFilter( // this.ScrollChain.filters.CommitBatch(index) // ); // const [finalEvent] = await this.ScrollChain.queryFilter( // this.ScrollChain.filters.FinalizeBatch(index) // ); const [[commitEvent], [finalEvent]] = await Promise.all([ this.ScrollChain.queryFilter(this.ScrollChain.filters.CommitBatch(index)), this.ScrollChain.queryFilter(this.ScrollChain.filters.FinalizeBatch(index)), ]); if (!commitEvent) throw new Error(`unknown batch`); if (!(finalEvent instanceof EventLog)) throw new Error('not finalized'); const tx = await commitEvent.getTransaction(); const desc = this.ScrollChain.interface.parseTransaction(tx); if (!desc) throw new Error(`expected commit tx: ${tx.hash}`); switch (desc.name) { case 'commitBatches': case 'commitAndFinalizeBatch': break; default: throw new Error(`unsupported commit tx: ${desc.name}`); } if (desc.args.version != BATCH_VERSION) { throw new Error(`unexpected version: ${desc.args.version}`); } if (!tx.blobVersionedHashes || !tx.blobVersionedHashes.length) { throw new Error(`expected blobs`); } const [config, block] = await Promise.all([ this.beaconConfig.get(), fetchBlock(this.provider1, tx.blockNumber), ]); const sidecars = await fetchSidecars(this.beaconAPI, (BigInt(block.timestamp) - config.genesisTime) / config.secondsPerSlot); let batchIndex = finalEvent.args.batchIndex - BigInt(tx.blobVersionedHashes.length - 1); let batchHash = desc.args.parentBatchHash; let sidecar; for (const bvh of tx.blobVersionedHashes) { sidecar = sidecars[bvh]; if (!sidecar) throw new Error(`expected sidecar: ${bvh}`); // https://github.com/scroll-tech/da-codec/blob/344f2d5e33e1930c63cd6a082ef77e27dbe50cea/encoding/codecv7.go#L168 // https://github.com/scroll-tech/da-codec/blob/344f2d5e33e1930c63cd6a082ef77e27dbe50cea/encoding/codecv7_types.go#L125 batchHash = keccak256(concat([ toPaddedHex(BATCH_VERSION, 1), toPaddedHex(batchIndex++, 8), bvh, batchHash, ])); } if (batchHash !== finalEvent.args.batchHash) { //desc.args.lastBatchHash) { throw new Error(`invalid batchHash chain: ${batchHash}`); } const prover = new EthProver(this.provider2, lastBlockFromBlobV7(sidecar.blob)); return { index, prover, l1BlockNumber: finalEvent.blockNumber }; } encodeWitness(commit, proofSeq) { return ABI_CODER.encode(['(uint256, bytes[], bytes)'], [[commit.index, proofSeq.proofs, proofSeq.order]]); } windowFromSec(sec) { // finalization time is not on-chain // https://etherscan.io/advanced-filter?eladd=0xa13baf47339d63b743e7da8741db5456dac1e556&eltpc=0x26ba82f907317eedc97d0cbef23de76a43dd6edb563bdb6e9407645b950a7a2d const span = 20; // every 10-20 batches const freq = 3600; // every hour? return span * Math.ceil(sec / freq); // units of batchIndex } } function makeBlobCanonical(blob) { // https://github.com/scroll-tech/da-codec/blob/344f2d5e33e1930c63cd6a082ef77e27dbe50cea/encoding/da.go#L469 const N = 4096; const v = new Uint8Array(N * 31); for (let i = 0; i < N; i++) { const offset = 4 + (i << 6); v.set(getBytes('0x' + blob.slice(offset, offset + 62)), i * 31); } return v; } function lastBlockFromBlobV7(blob) { //https://github.com/scroll-tech/da-codec/blob/344f2d5e33e1930c63cd6a082ef77e27dbe50cea/encoding/codecv7.go#L176 let v = makeBlobCanonical(blob); if (v[0] != BATCH_VERSION) { throw new Error(`unexpected version: ${v[0]}`); } const compressed = v[4]; if (compressed != 0 && compressed != 1) { throw new Error(`unexpected compression: ${v[4]}`); } const size = (v[1] << 16) | (v[2] << 8) | v[3]; // uint24 if (compressed) { v = v.slice(1, 5 + size); // https://github.com/scroll-tech/da-codec/blob/344f2d5e33e1930c63cd6a082ef77e27dbe50cea/encoding/da.go#L50 v[0] = 0x28; // zstdMagicNumber v[1] = 0xb5; v[2] = 0x2f; v[3] = 0xfd; v = decompress(v); } else { v = v.subarray(5, 5 + size); } // https://github.com/scroll-tech/da-codec/blob/344f2d5e33e1930c63cd6a082ef77e27dbe50cea/encoding/codecv7_types.go#L275 if (v.length < 74) throw new Error(`payload too small: ${v.length}`); // blobPayloadV7MinEncodedLength const dv = new DataView(v.buffer, v.byteOffset, v.byteLength); const l2BlockNumber = dv.getBigUint64(64); // blobPayloadV7OffsetInitialL2BlockNumber const numBlocks = dv.getUint16(72); // blobPayloadV7OffsetNumBlocks return l2BlockNumber + BigInt(numBlocks - 1); }