UNPKG

@tokamak-network/thanos-sdk

Version:
549 lines 25.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Portals = void 0; const abstract_provider_1 = require("@ethersproject/abstract-provider"); const ethers_1 = require("ethers"); const core_utils_1 = require("@tokamak-network/core-utils"); const semver_1 = __importDefault(require("semver")); const interfaces_1 = require("./interfaces"); const utils_1 = require("./utils"); class Portals { constructor(opts) { var _a; this._outputCache = []; this.populateTransaction = { depositTransaction: async (request) => { return this.contracts.l1.OptimismPortal.populateTransaction.depositTransaction(request.to, request.mint, request.value, request.gasLimit, request.isCreation, request.data); }, initiateWithdrawal: async (request) => { return this.contracts.l2.BedrockMessagePasser.populateTransaction.initiateWithdrawal(request.target, request.gasLimit, request.data, { value: request.value, }); }, proveWithdrawalTransaction: async (message, opts) => { const proof = await this.getBedrockMessageProof(message); const args = [ [ message.messageNonce, message.sender, message.target, message.value, message.minGasLimit, message.message, ], proof.l2OutputIndex, [ proof.outputRootProof.version, proof.outputRootProof.stateRoot, proof.outputRootProof.messagePasserStorageRoot, proof.outputRootProof.latestBlockhash, ], proof.withdrawalProof, (opts === null || opts === void 0 ? void 0 : opts.overrides) || {}, ]; return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(...args); }, finalizeWithdrawalTransaction: async (message) => { return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction([ message.messageNonce, message.sender, message.target, message.value, message.minGasLimit, message.message, ]); }, }; this.estimateGas = { depositTransaction: async (request) => { const tx = await this.populateTransaction.depositTransaction(request); return this.l1Provider.estimateGas(tx); }, initiateWithdrawal: async (request) => { const tx = await this.populateTransaction.initiateWithdrawal(request); return this.l2Provider.estimateGas(tx); }, proveWithdrawalTransaction: async (message, opts) => { const tx = await this.populateTransaction.proveWithdrawalTransaction(message, opts); return this.l1Provider.estimateGas(tx); }, finalizeWithdrawalTransaction: async (message) => { const tx = await this.populateTransaction.finalizeWithdrawalTransaction(message); return this.l1Provider.estimateGas(tx); }, }; this.bedrock = (_a = opts.bedrock) !== null && _a !== void 0 ? _a : true; this.l1SignerOrProvider = (0, utils_1.toSignerOrProvider)(opts.l1SignerOrProvider); this.l2SignerOrProvider = (0, utils_1.toSignerOrProvider)(opts.l2SignerOrProvider); try { this.l1ChainId = (0, utils_1.toNumber)(opts.l1ChainId); } catch (err) { throw new Error(`L1 chain ID is missing or invalid: ${opts.l1ChainId}`); } try { this.l2ChainId = (0, utils_1.toNumber)(opts.l2ChainId); } catch (err) { throw new Error(`L2 chain ID is missing or invalid: ${opts.l2ChainId}`); } this.depositConfirmationBlocks = (opts === null || opts === void 0 ? void 0 : opts.depositConfirmationBlocks) !== undefined ? (0, utils_1.toNumber)(opts.depositConfirmationBlocks) : utils_1.DEPOSIT_CONFIRMATION_BLOCKS[this.l2ChainId] || 0; this.l1BlockTimeSeconds = (opts === null || opts === void 0 ? void 0 : opts.l1BlockTimeSeconds) !== undefined ? (0, utils_1.toNumber)(opts.l1BlockTimeSeconds) : utils_1.CHAIN_BLOCK_TIMES[this.l1ChainId] || 1; this.contracts = (0, utils_1.getPortalsContracts)(this.l2ChainId, { l1SignerOrProvider: this.l1SignerOrProvider, l2SignerOrProvider: this.l2SignerOrProvider, overrides: opts.contracts, }); } get l1Provider() { if (abstract_provider_1.Provider.isProvider(this.l1SignerOrProvider)) { return this.l1SignerOrProvider; } else { return this.l1SignerOrProvider.provider; } } get l2Provider() { if (abstract_provider_1.Provider.isProvider(this.l2SignerOrProvider)) { return this.l2SignerOrProvider; } else { return this.l2SignerOrProvider.provider; } } get l1Signer() { if (abstract_provider_1.Provider.isProvider(this.l1SignerOrProvider)) { throw new Error(`messenger has no L1 signer`); } else { return this.l1SignerOrProvider; } } get l2Signer() { if (abstract_provider_1.Provider.isProvider(this.l2SignerOrProvider)) { throw new Error(`messenger has no L2 signer`); } else { return this.l2SignerOrProvider; } } async getL1BlockNumber() { return this.contracts.l2.OVM_L1BlockNumber.getL1BlockNumber(); } async getMessageStatus(txReceipt) { return txReceipt.to === core_utils_1.predeploys.L2ToL1MessagePasser ? this.getL2ToL1MessageStatusByReceipt(txReceipt) : this.getL1ToL2MessageStatusByReceipt(txReceipt); } async getL1ToL2MessageStatusByReceipt(txReceipt) { const l1BlockNumber = await this.getL1BlockNumber(); return txReceipt.blockNumber > l1BlockNumber ? interfaces_1.MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE : interfaces_1.MessageStatus.RELAYED; } async getStateBatchAppendedEventByBatchIndex(batchIndex) { const events = await this.contracts.l1.StateCommitmentChain.queryFilter(this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(batchIndex)); if (events.length === 0) { return null; } else if (events.length > 1) { throw new Error(`found more than one StateBatchAppended event`); } else { return events[0]; } } async getStateBatchAppendedEventByTransactionIndex(transactionIndex) { const isEventHi = (event, index) => { const prevTotalElements = event.args._prevTotalElements.toNumber(); return index < prevTotalElements; }; const isEventLo = (event, index) => { const prevTotalElements = event.args._prevTotalElements.toNumber(); const batchSize = event.args._batchSize.toNumber(); return index >= prevTotalElements + batchSize; }; const totalBatches = await this.contracts.l1.StateCommitmentChain.getTotalBatches(); if (totalBatches.eq(0)) { return null; } let lowerBound = 0; let upperBound = totalBatches.toNumber() - 1; let batchEvent = await this.getStateBatchAppendedEventByBatchIndex(upperBound); if (batchEvent === null) { return null; } if (isEventLo(batchEvent, transactionIndex)) { return null; } else if (!isEventHi(batchEvent, transactionIndex)) { return batchEvent; } while (lowerBound < upperBound) { const middleOfBounds = Math.floor((lowerBound + upperBound) / 2); batchEvent = await this.getStateBatchAppendedEventByBatchIndex(middleOfBounds); if (isEventHi(batchEvent, transactionIndex)) { upperBound = middleOfBounds; } else if (isEventLo(batchEvent, transactionIndex)) { lowerBound = middleOfBounds; } else { break; } } return batchEvent; } async getStateRootBatchByTransactionIndex(transactionIndex) { const stateBatchAppendedEvent = await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex); if (stateBatchAppendedEvent === null) { return null; } const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction(); const [stateRoots] = this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData('appendStateBatch', stateBatchTransaction.data); return { blockNumber: stateBatchAppendedEvent.blockNumber, stateRoots, header: { batchIndex: stateBatchAppendedEvent.args._batchIndex, batchRoot: stateBatchAppendedEvent.args._batchRoot, batchSize: stateBatchAppendedEvent.args._batchSize, prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements, extraData: stateBatchAppendedEvent.args._extraData, }, }; } async getMessageStateRoot(transactionHash) { const messageTxReceipt = await this.l2Provider.getTransactionReceipt(transactionHash); const messageTxIndex = messageTxReceipt.blockNumber - 1; const stateRootBatch = await this.getStateRootBatchByTransactionIndex(messageTxIndex); if (stateRootBatch === null) { return null; } const indexInBatch = messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber(); if (stateRootBatch.stateRoots.length <= indexInBatch) { throw new Error(`state root does not exist in batch`); } return { stateRoot: stateRootBatch.stateRoots[indexInBatch], stateRootIndexInBatch: indexInBatch, batch: stateRootBatch, }; } async getMessageBedrockOutput(l2BlockNumber) { let proposal; let l2OutputIndex; if (await this.fpac()) { const gameType = await this.contracts.l1.OptimismPortal2.respectedGameType(); const gameCount = await this.contracts.l1.DisputeGameFactory.gameCount(); const latestGames = await this.contracts.l1.DisputeGameFactory.findLatestGames(gameType, Math.max(0, gameCount.sub(1).toNumber()), Math.min(100, gameCount.toNumber())); const matches = []; for (const game of latestGames) { try { const [blockNumber] = ethers_1.ethers.utils.defaultAbiCoder.decode(['uint256'], game.extraData); if (blockNumber.gte(l2BlockNumber)) { matches.push(Object.assign(Object.assign({}, game), { l2BlockNumber: blockNumber })); } } catch (err) { continue; } } for (let i = matches.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [matches[i], matches[j]] = [matches[j], matches[i]]; } let match; for (const option of matches) { if (await this.isValidOutputRoot(option.rootClaim, option.l2BlockNumber)) { match = option; break; } } if (!match) { return null; } l2OutputIndex = match.index; proposal = { outputRoot: match.rootClaim, timestamp: match.timestamp, l2BlockNumber: match.l2BlockNumber, }; } else { try { l2OutputIndex = await this.contracts.l1.L2OutputOracle.getL2OutputIndexAfter(l2BlockNumber); } catch (err) { if (err.message.includes('L2OutputOracle: cannot get output')) { return null; } else { throw err; } } proposal = await this.contracts.l1.L2OutputOracle.getL2Output(l2OutputIndex); } return { outputRoot: proposal.outputRoot, l1Timestamp: proposal.timestamp.toNumber(), l2BlockNumber: proposal.l2BlockNumber.toNumber(), l2OutputIndex: l2OutputIndex.toNumber(), }; } async getL2ToL1MessageStatusByReceipt(txReceipt) { const withdrawalMessageInfo = await this.calculateWithdrawalMessage(txReceipt); if (!withdrawalMessageInfo) { throw Error('withdrawal message not found'); } const finalizedMessage = await this.getFinalizedWithdrawalStatus(withdrawalMessageInfo.withdrawalHash); if (finalizedMessage) { return interfaces_1.MessageStatus.RELAYED; } let timestamp; if (this.bedrock) { const output = await this.getMessageBedrockOutput(withdrawalMessageInfo.l2BlockNumber); if (output === null) { return interfaces_1.MessageStatus.STATE_ROOT_NOT_PUBLISHED; } const provenWithdrawal = await this.getProvenWithdrawal(withdrawalMessageInfo.withdrawalHash); if (!provenWithdrawal || provenWithdrawal.timestamp.toNumber() === 0) { return interfaces_1.MessageStatus.READY_TO_PROVE; } timestamp = provenWithdrawal.timestamp.toNumber(); } else { const stateRoot = await this.getMessageStateRoot(txReceipt.transactionHash); if (stateRoot === null) { return interfaces_1.MessageStatus.STATE_ROOT_NOT_PUBLISHED; } const bn = stateRoot.batch.blockNumber; const block = await this.l1Provider.getBlock(bn); timestamp = block.timestamp; } if (await this.fpac()) { const provenWithdrawal = await this.getProvenWithdrawal(withdrawalMessageInfo.withdrawalHash); if (provenWithdrawal === null) { console.warn('Unexpected code path reached in getMessageStatus, returning READY_TO_PROVE'); return interfaces_1.MessageStatus.READY_TO_PROVE; } if (!('proofSubmitter' in provenWithdrawal)) { throw new Error(`expected to get FPAC withdrawal but got legacy withdrawal`); } try { await this.contracts.l1.OptimismPortal2.checkWithdrawal((0, utils_1.hashLowLevelMessage)(withdrawalMessageInfo), provenWithdrawal.proofSubmitter); return interfaces_1.MessageStatus.READY_FOR_RELAY; } catch (err) { return interfaces_1.MessageStatus.IN_CHALLENGE_PERIOD; } } else { const challengePeriod = await this.getChallengePeriodSeconds(); const latestBlock = await this.l1Provider.getBlock('latest'); if (timestamp + challengePeriod > latestBlock.timestamp) { return interfaces_1.MessageStatus.IN_CHALLENGE_PERIOD; } else { return interfaces_1.MessageStatus.READY_FOR_RELAY; } } } async calculateRelayedDepositTxID(txReceipt) { if (txReceipt.status !== 1) { return null; } let promiseString = null; txReceipt.logs.forEach((log) => { if (log.topics[0] === ethers_1.ethers.utils.id('TransactionDeposited(address,address,uint256,bytes)')) { const depositTx = core_utils_1.DepositTx.fromL1Log(log); promiseString = Promise.resolve(depositTx.hash()); } }); return promiseString; } async calculateWithdrawalMessage(txReceipt) { if (txReceipt.status !== 1) { return null; } let promiseMessage = null; const withdrawalMessage = (0, utils_1.calculateWithdrawalMessageUsingRecept)(txReceipt); promiseMessage = Promise.resolve(withdrawalMessage); return promiseMessage; } async calculateWithdrawalMessageByL2TxHash(transactionHash) { const txReceipt = await this.l2Provider.getTransactionReceipt(transactionHash); return this.calculateWithdrawalMessage(txReceipt); } async waitForMessageStatus(txReceipt, status, opts) { let totalTimeMs = 0; while (totalTimeMs < ((opts === null || opts === void 0 ? void 0 : opts.timeoutMs) || Infinity)) { const tick = Date.now(); const currentStatus = await this.getMessageStatus(txReceipt); if (currentStatus >= status) { return; } await (0, core_utils_1.sleep)((opts === null || opts === void 0 ? void 0 : opts.pollIntervalMs) || 1000); totalTimeMs += Date.now() - tick; } throw new Error(`timed out waiting for relayed deposit transaction`); } async getChallengePeriodSeconds() { if (!this.bedrock) { return (await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()).toNumber(); } const oracleVersion = await this.contracts.l1.L2OutputOracle.version(); const challengePeriod = oracleVersion === '1.0.0' ? ethers_1.BigNumber.from(await this.contracts.l1.OptimismPortal.provider.call({ to: this.contracts.l1.OptimismPortal.address, data: '0xf4daa291', })) : await this.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS(); return challengePeriod.toNumber(); } async getBedrockMessageProof(withdrawalMessageInfo) { const output = await this.getMessageBedrockOutput(withdrawalMessageInfo.l2BlockNumber); if (output === null) { throw new Error(`state root for message not yet published`); } const hash = (0, utils_1.hashLowLevelMessage)(withdrawalMessageInfo); const messageSlot = (0, utils_1.hashMessageHash)(hash); const provider = (0, utils_1.toJsonRpcProvider)(this.l2Provider); const stateTrieProof = await (0, utils_1.makeStateTrieProof)(provider, output.l2BlockNumber, this.contracts.l2.BedrockMessagePasser.address, messageSlot); const block = await provider.send('eth_getBlockByNumber', [ (0, core_utils_1.toRpcHexString)(output.l2BlockNumber), false, ]); return { outputRootProof: { version: ethers_1.ethers.constants.HashZero, stateRoot: block.stateRoot, messagePasserStorageRoot: stateTrieProof.storageRoot, latestBlockhash: block.hash, }, withdrawalProof: stateTrieProof.storageProof, l2OutputIndex: output.l2OutputIndex, }; } async getProvenWithdrawal(withdrawalHash) { if (!this.bedrock) { throw new Error('message proving only applies after the bedrock upgrade'); } if (await this.fpac()) { const numProofSubmitters = ethers_1.BigNumber.from(await this.contracts.l1.OptimismPortal2.numProofSubmitters(withdrawalHash)).toNumber(); for (let i = 0; i < numProofSubmitters; i++) { const proofSubmitter = await this.contracts.l1.OptimismPortal2.proofSubmitters(withdrawalHash, i); const provenWithdrawal = await this.contracts.l1.OptimismPortal2.provenWithdrawals(withdrawalHash, proofSubmitter); const game = new ethers_1.ethers.Contract(provenWithdrawal.disputeGameProxy, (0, utils_1.getContractInterfaceBedrock)('FaultDisputeGame'), this.l1SignerOrProvider); const status = await game.status(); if (status === 1) { continue; } else if (status === 2) { return Object.assign(Object.assign({}, provenWithdrawal), { proofSubmitter }); } else if (status > 2) { throw new Error('got invalid game status'); } const extraData = await game.extraData(); let l2BlockNumber; try { ; [l2BlockNumber] = ethers_1.ethers.utils.defaultAbiCoder.decode(['uint256'], extraData); } catch (err) { continue; } if (await this.isValidOutputRoot(await game.rootClaim(), l2BlockNumber)) { return Object.assign(Object.assign({}, provenWithdrawal), { proofSubmitter }); } } return null; } else { return this.contracts.l1.OptimismPortal.provenWithdrawals(withdrawalHash); } } async isValidOutputRoot(outputRoot, l2BlockNumber) { const cached = this._outputCache.find((other) => { return other.root === outputRoot; }); if (cached) { return cached.valid; } if (this._outputCache.length > 10000) { this._outputCache = this._outputCache.slice(5000); } try { const provider = (0, utils_1.toJsonRpcProvider)(this.l2Provider); const [block, proof] = await Promise.all([ provider.send('eth_getBlockByNumber', [ (0, core_utils_1.toRpcHexString)(l2BlockNumber), false, ]), (0, utils_1.makeStateTrieProof)(provider, l2BlockNumber, this.contracts.l2.OVM_L2ToL1MessagePasser.address, ethers_1.ethers.constants.HashZero), ]); const output = ethers_1.ethers.utils.solidityKeccak256(['bytes32', 'bytes32', 'bytes32', 'bytes32'], [ ethers_1.ethers.constants.HashZero, block.stateRoot, proof.storageRoot, block.hash, ]); const valid = output === outputRoot; this._outputCache.push({ root: outputRoot, valid }); return valid; } catch (err) { return false; } } async getFinalizedWithdrawalStatus(withdrawalHash) { return this.contracts.l1.OptimismPortal.finalizedWithdrawals(withdrawalHash); } async depositTransaction(request, opts) { const tx = await this.populateTransaction.depositTransaction(request); return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l1Signer).sendTransaction(tx); } async initiateWithdrawal(request, opts) { const tx = await this.populateTransaction.initiateWithdrawal(request); return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l2Signer).sendTransaction(tx); } async proveWithdrawalTransaction(message, opts) { const tx = await this.populateTransaction.proveWithdrawalTransaction(message); return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l1Signer).sendTransaction(tx); } async proveWithdrawalTransactionUsingL2Tx(transactionHash, opts) { const message = await this.calculateWithdrawalMessageByL2TxHash(transactionHash); return this.proveWithdrawalTransaction(message, opts); } async finalizeWithdrawalTransaction(message, opts) { const tx = await this.populateTransaction.finalizeWithdrawalTransaction(message); return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l1Signer).sendTransaction(tx); } async finalizeWithdrawalTransactionUsingL2Tx(transactionHash, opts) { const message = await this.calculateWithdrawalMessageByL2TxHash(transactionHash); return this.finalizeWithdrawalTransaction(message); } async fpac() { if (this.contracts.l1.OptimismPortal.address === ethers_1.ethers.constants.AddressZero) { return false; } else { return semver_1.default.gte(await this.contracts.l1.OptimismPortal.version(), '3.0.0'); } } } exports.Portals = Portals; //# sourceMappingURL=portals.js.map