@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
296 lines (295 loc) • 12.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoLDRollup = exports.ROLLUP_ABI = exports.MACHINE_STATUS_FINISHED = exports.ASSERTION_STATUS_CONFIRMED = void 0;
const ArbitrumRollup_js_1 = require("./ArbitrumRollup.cjs");
const chains_js_1 = require("../chains.cjs");
const contract_1 = require("ethers/contract");
const abi_1 = require("ethers/abi");
const crypto_1 = require("ethers/crypto");
const utils_1 = require("ethers/utils");
const EthProver_js_1 = require("../eth/EthProver.cjs");
const utils_js_1 = require("../utils.cjs");
const rlp_js_1 = require("../rlp.cjs");
const cached_js_1 = require("../cached.cjs");
// https://docs.arbitrum.io/how-arbitrum-works/bold/gentle-introduction
// https://github.com/OffchainLabs/bold
// https://github.com/OffchainLabs/nitro-contracts/blob/94999b3e2d3b4b7f8e771cc458b9eb229620dd8f/src/rollup/Assertion.sol
exports.ASSERTION_STATUS_CONFIRMED = 2n;
// https://github.com/OffchainLabs/nitro-contracts/blob/94999b3e2d3b4b7f8e771cc458b9eb229620dd8f/src/state/Machine.sol
exports.MACHINE_STATUS_FINISHED = 1n;
exports.ROLLUP_ABI = new abi_1.Interface([
`function latestConfirmed() view returns (bytes32)`,
`function confirmPeriodBlocks() view returns (uint256)`,
`function getAssertion(bytes32) view returns ((
uint64 firstChildBlock,
uint64 secondChildBlock,
uint64 createdAtBlock,
bool isFirstChild,
uint8 status,
bytes32 configHash
))`,
`event AssertionConfirmed(
bytes32 indexed assertionHash,
bytes32 blockHash,
bytes32 sendRoot
)`,
`event AssertionCreated(
bytes32 indexed assertionHash,
bytes32 indexed parentAssertionHash,
(
(
bytes32 prevPrevAssertionHash,
bytes32 sequencerBatchAcc,
(
bytes32 wasmModuleRoot,
uint256 requiredStake,
address challengeManager,
uint64 confirmPeriodBlocks,
uint64 nextInboxPosition
) configData
) beforeStateData,
(
(
bytes32[2] bytes32Vals,
uint64[2] u64Vals
) globalState,
uint8 machineStatus,
bytes32 endHistoryRoot
) beforeState,
(
(
bytes32[2] bytes32Vals,
uint64[2] u64Vals
) globalState,
uint8 machineStatus,
bytes32 endHistoryRoot
) afterState
) assertion,
bytes32 afterInboxBatchAcc,
uint256 inboxMaxCount,
bytes32 wasmModuleRoot,
uint256 requiredStake,
address challengeManager,
uint64 confirmPeriodBlocks
)`,
]);
function knownFromCreatedEvent(event) {
return {
createdAtBlock: BigInt(event.blockNumber),
assertionHash: event.args.assertionHash,
parentAssertionHash: event.args.parentAssertionHash,
afterState: event.args.assertion.afterState,
afterInboxBatchAcc: event.args.afterInboxBatchAcc,
confirmed: false,
children: 0,
};
}
function isValidAssertionChain(chain) {
return (chain.length >= 2 &&
chain[0].confirmed &&
chain[chain.length - 1].afterState.machineStatus ==
exports.MACHINE_STATUS_FINISHED &&
chain.slice(0, -1).every((x) => x.children < 2));
}
class BoLDRollup extends ArbitrumRollup_js_1.AbstractArbitrumRollup {
// TODO: get docs link once arbitrum updates their website
static arb1MainnetConfig = {
chain1: chains_js_1.CHAINS.MAINNET,
chain2: chains_js_1.CHAINS.ARB1,
Rollup: '0x4DCeB440657f21083db8aDd07665f8ddBe1DCfc0',
isBoLD: true,
};
static arb1SepoliaConfig = {
chain1: chains_js_1.CHAINS.SEPOLIA,
chain2: chains_js_1.CHAINS.ARB1_SEPOLIA,
Rollup: '0x042B2E6C5E99d4c521bd49beeD5E99651D9B0Cf4',
isBoLD: true,
};
static arbNovaMainnetConfig = {
chain1: chains_js_1.CHAINS.MAINNET,
chain2: chains_js_1.CHAINS.ARB_NOVA,
Rollup: '0xE7E8cCC7c381809BDC4b213CE44016300707B7Bd',
isBoLD: true,
};
constructor(providers, config, minAgeBlocks = 0) {
super(providers, true, config, exports.ROLLUP_ABI, minAgeBlocks);
}
async _fetchNode(assertionHash) {
return this.Rollup.getAssertion(assertionHash);
}
_lastBlock = -1n;
_assertionMap = new Map();
unfinalizedGuard = new cached_js_1.CachedValue(async () => {
const latest = await (0, utils_js_1.fetchBlockNumber)(this.provider1, this.latestBlockTag);
const block1 = latest - BigInt(this.minAgeBlocks);
let block0 = this._lastBlock + 1n;
if (!block0) {
const assertionHash = await this.Rollup.latestConfirmed({
blockTag: block1,
});
const node = await this._fetchNode(assertionHash);
if (!node.status) {
throw new Error(`expected latest assertion: ${assertionHash}`);
}
block0 = node.createdAtBlock;
}
await this._syncAssertions(block0, block1);
this._lastBlock = block1;
}, 60000);
async _syncAssertions(block, block1) {
const topics = [
[
this.Rollup.filters.AssertionCreated().fragment.topicHash,
this.Rollup.filters.AssertionConfirmed().fragment.topicHash,
],
];
// this is processed by increasing time (insertion order for Map)
const inclusiveStep = BigInt(this.getLogsStepSize - 1);
while (block <= block1) {
let next = block + inclusiveStep;
if (next > block1)
next = block1;
const events = await this.Rollup.queryFilter(topics, block, next);
for (const e of events) {
if (e instanceof contract_1.EventLog) {
if (e.eventName == 'AssertionCreated') {
const known = knownFromCreatedEvent(e);
const parent = this._assertionMap.get(known.parentAssertionHash);
if (parent)
parent.children++;
this._assertionMap.set(known.assertionHash, known);
}
else {
const known = this._assertionMap.get(e.args.assertionHash);
if (known) {
known.confirmed = true;
// we only need to keep 1 confirmed assertion
for (const key of this._assertionMap.keys()) {
if (key == known.assertionHash)
break;
this._assertionMap.delete(key);
}
}
}
}
}
block = next + 1n;
}
}
_latestAssertions(fn) {
return Array.from(this._assertionMap.values())
.filter((x) => fn(x.createdAtBlock))
.reverse(); // ordered by descending createdAtBlock
}
async _unfinalizedAssertionChain(assertionHash) {
await this.unfinalizedGuard.get();
const chain = [];
for (;;) {
const known = this._assertionMap.get(assertionHash);
if (!known)
break; // broken chain
chain.push(known);
if (known.confirmed)
break; // complete chain
assertionHash = known.parentAssertionHash;
}
// returns sequence of assertions from oldest (confirmed?) to newest
// NOTE: may not be a valid chain
return chain.reverse();
}
async fetchLatestCommitIndex() {
let blockTag = this.latestBlockTag;
if (this.minAgeBlocks) {
await this.unfinalizedGuard.get();
const block = this._lastBlock;
for (const known of this._latestAssertions((x) => x <= block)) {
const chain = await this._unfinalizedAssertionChain(known.assertionHash);
if (isValidAssertionChain(chain)) {
return known.createdAtBlock;
}
}
blockTag = block;
}
const assertionHash = await this.Rollup.latestConfirmed({
blockTag,
});
const node = await this._fetchNode(assertionHash);
if (!node.status)
throw new Error(`expected latest assertion`);
return node.createdAtBlock;
}
async _fetchParentCommitIndex(commit) {
const node = await this._fetchNode(commit.assertions[commit.assertions.length - 2]);
return node.status ? node.createdAtBlock : -1n;
}
async _findAssertionChainAtIndex(index) {
if (this.minAgeBlocks) {
await this.unfinalizedGuard.get();
for (const known of this._latestAssertions((x) => x == index)) {
const chain = await this._unfinalizedAssertionChain(known.assertionHash);
if (isValidAssertionChain(chain))
return chain;
}
}
const events = await this.Rollup.queryFilter(this.Rollup.filters.AssertionCreated(), index, index);
// most likely there aren't 2+ assertions in 1 block...
for (let i = events.length - 1; i >= 0; i--) {
const event = events[i];
if (event instanceof contract_1.EventLog &&
event.args.assertion.afterState.machineStatus == exports.MACHINE_STATUS_FINISHED) {
const [node, [parentEvent]] = await Promise.all([
this._fetchNode(event.args.assertionHash),
this.Rollup.queryFilter(this.Rollup.filters.AssertionCreated(event.args.parentAssertionHash)),
]);
if (node.status == exports.ASSERTION_STATUS_CONFIRMED &&
parentEvent instanceof contract_1.EventLog) {
const parent = knownFromCreatedEvent(parentEvent);
parent.confirmed = true; // by construction
const child = knownFromCreatedEvent(event);
child.confirmed = true; // by status assertion
// isValidAssertionChain() is true by construction
// parent.children = 1; // not needed
return [parent, child];
}
}
}
throw new Error('invalid assertion chain');
}
async _fetchCommit(index) {
const chain = await this._findAssertionChainAtIndex(index);
const { afterState, confirmed } = chain[chain.length - 1];
const blockHash = afterState.globalState[0][0];
const block = await (0, utils_js_1.fetchBlockFromHash)(this.provider2, blockHash);
const encodedRollupProof = utils_js_1.ABI_CODER.encode(['(bytes32, bytes, ((bytes32[2], uint64[2]), uint8, bytes32), bytes)'], [
[
chain[0].assertionHash,
encodeAssertionChain(chain.slice(1)),
afterState,
(0, rlp_js_1.encodeRlpBlock)(block),
],
]);
return {
index,
prover: new EthProver_js_1.EthProver(this.provider2, block.number),
confirmed,
assertions: chain.map((x) => x.assertionHash),
encodedRollupProof,
};
}
async isCommitStillValid(commit) {
if (commit.confirmed)
return true;
// one of the links may have been challenged
const chain = await this._findAssertionChainAtIndex(commit.index);
return chain.every((x, i) => commit.assertions[i] == x.assertionHash);
}
}
exports.BoLDRollup = BoLDRollup;
function encodeAssertionChain(chain) {
// starting from hash[0] (confirmed), we need to construct:
// hash[n] = keccak(encode(hash[n-1], keccak256(abi.encode(p.afterState)), p.inboxAcc)
return (0, utils_1.concat)(chain.flatMap((known) => [
(0, crypto_1.keccak256)(utils_js_1.ABI_CODER.encode(['((bytes32[2], uint64[2]), uint8, bytes32)'], [known.afterState])),
known.afterInboxBatchAcc,
]));
}