UNPKG

@arbitrum/sdk

Version:

Typescript library client-side interactions with Arbitrum

486 lines (485 loc) 24.8 kB
/* * Copyright 2021, Offchain Labs, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-env node */ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChildToParentMessageWriterNitro = exports.ChildToParentMessageReaderNitro = exports.ChildToParentMessageNitro = void 0; const constants_1 = require("../dataEntities/constants"); const bignumber_1 = require("@ethersproject/bignumber"); const logger_1 = require("@ethersproject/logger"); const ArbSys__factory_1 = require("../abi/factories/ArbSys__factory"); const RollupUserLogic__factory_1 = require("../abi/factories/RollupUserLogic__factory"); const BoldRollupUserLogic__factory_1 = require("../abi-bold/factories/BoldRollupUserLogic__factory"); const Outbox__factory_1 = require("../abi/factories/Outbox__factory"); const NodeInterface__factory_1 = require("../abi/factories/NodeInterface__factory"); const async_mutex_1 = require("async-mutex"); const eventFetcher_1 = require("../utils/eventFetcher"); const errors_1 = require("../dataEntities/errors"); const signerOrProvider_1 = require("../dataEntities/signerOrProvider"); const lib_1 = require("../utils/lib"); const networks_1 = require("../dataEntities/networks"); const arbProvider_1 = require("../utils/arbProvider"); const message_1 = require("../dataEntities/message"); const Bridge__factory_1 = require("../abi/factories/Bridge__factory"); // expected number of parent chain blocks that it takes for a Child chain tx to be included in a parent chain assertion const ASSERTION_CREATED_PADDING = 50; // expected number of parent blocks that it takes for a validator to confirm a parent block after the assertion deadline is passed const ASSERTION_CONFIRMED_PADDING = 20; const childBlockRangeCache = {}; const mutex = new async_mutex_1.Mutex(); function getChildBlockRangeCacheKey({ childChainId, l1BlockNumber, }) { return `${childChainId}-${l1BlockNumber}`; } function setChildBlockRangeCache(key, value) { childBlockRangeCache[key] = value; } async function getBlockRangesForL1BlockWithCache({ parentProvider, childProvider, forL1Block, }) { const childChainId = (await childProvider.getNetwork()).chainId; const key = getChildBlockRangeCacheKey({ childChainId, l1BlockNumber: forL1Block, }); if (childBlockRangeCache[key]) { return childBlockRangeCache[key]; } // implements a lock that only fetches cache once const release = await mutex.acquire(); // if cache has been acquired while awaiting the lock if (childBlockRangeCache[key]) { release(); return childBlockRangeCache[key]; } try { const childBlockRange = await (0, lib_1.getBlockRangesForL1Block)({ forL1Block, arbitrumProvider: parentProvider, }); setChildBlockRangeCache(key, childBlockRange); } finally { release(); } return childBlockRangeCache[key]; } /** * Base functionality for nitro Child->Parent messages */ class ChildToParentMessageNitro { constructor(event) { this.event = event; } static fromEvent(parentSignerOrProvider, event, parentProvider) { return signerOrProvider_1.SignerProviderUtils.isSigner(parentSignerOrProvider) ? new ChildToParentMessageWriterNitro(parentSignerOrProvider, event, parentProvider) : new ChildToParentMessageReaderNitro(parentSignerOrProvider, event); } static async getChildToParentEvents(childProvider, filter, position, destination, hash) { const eventFetcher = new eventFetcher_1.EventFetcher(childProvider); return (await eventFetcher.getEvents(ArbSys__factory_1.ArbSys__factory, t => t.filters.L2ToL1Tx(null, destination, hash, position), Object.assign(Object.assign({}, filter), { address: constants_1.ARB_SYS_ADDRESS }))).map(l => (Object.assign(Object.assign({}, l.event), { transactionHash: l.transactionHash }))); } } exports.ChildToParentMessageNitro = ChildToParentMessageNitro; /** * Provides read-only access nitro for child-to-parent-messages */ class ChildToParentMessageReaderNitro extends ChildToParentMessageNitro { constructor(parentProvider, event) { super(event); this.parentProvider = parentProvider; } async getOutboxProof(childProvider) { const { sendRootSize } = await this.getSendProps(childProvider); if (!sendRootSize) throw new errors_1.ArbSdkError('Assertion not yet created, cannot get proof.'); const nodeInterface = NodeInterface__factory_1.NodeInterface__factory.connect(constants_1.NODE_INTERFACE_ADDRESS, childProvider); const outboxProofParams = await nodeInterface.callStatic.constructOutboxProof(sendRootSize.toNumber(), this.event.position.toNumber()); return outboxProofParams.proof; } /** * Check if this message has already been executed in the Outbox */ async hasExecuted(childProvider) { const childChain = await (0, networks_1.getArbitrumNetwork)(childProvider); const outbox = Outbox__factory_1.Outbox__factory.connect(childChain.ethBridge.outbox, this.parentProvider); return outbox.callStatic.isSpent(this.event.position); } /** * Get the status of this message * In order to check if the message has been executed proof info must be provided. * @returns */ async status(childProvider) { const { sendRootConfirmed } = await this.getSendProps(childProvider); if (!sendRootConfirmed) return message_1.ChildToParentMessageStatus.UNCONFIRMED; return (await this.hasExecuted(childProvider)) ? message_1.ChildToParentMessageStatus.EXECUTED : message_1.ChildToParentMessageStatus.CONFIRMED; } parseNodeCreatedAssertion(event) { return { afterState: { blockHash: event.event.assertion.afterState.globalState.bytes32Vals[0], sendRoot: event.event.assertion.afterState.globalState.bytes32Vals[1], }, }; } parseAssertionCreatedEvent(e) { return { afterState: { blockHash: e.event.assertion .afterState.globalState.bytes32Vals[0], sendRoot: e.event.assertion .afterState.globalState.bytes32Vals[1], }, }; } isAssertionCreatedLog(log) { return (log.event.challengeManager != undefined); } async getBlockFromAssertionLog(childProvider, log) { const arbitrumProvider = new arbProvider_1.ArbitrumProvider(childProvider); if (!log) { console.warn('No AssertionCreated events found, defaulting to block 0'); return arbitrumProvider.getBlock(0); } const parsedLog = this.isAssertionCreatedLog(log) ? this.parseAssertionCreatedEvent(log) : this.parseNodeCreatedAssertion(log); // if the chain is freshly deployed and latest confirmed/created is the genesis assertion that contains a empty block hash if (parsedLog.afterState.blockHash === '0x0000000000000000000000000000000000000000000000000000000000000000') { return arbitrumProvider.getBlock(0); } const childBlock = await arbitrumProvider.getBlock(parsedLog.afterState.blockHash); if (!childBlock) { throw new errors_1.ArbSdkError(`Block not found. ${parsedLog.afterState.blockHash}`); } if (childBlock.sendRoot !== parsedLog.afterState.sendRoot) { throw new errors_1.ArbSdkError(`Child chain block send root doesn't match parsed log. ${childBlock.sendRoot} ${parsedLog.afterState.sendRoot}`); } return childBlock; } isBoldRollupUserLogic(rollup) { return rollup.getAssertion !== undefined; } async getBlockFromAssertionId(rollup, assertionId, childProvider) { const createdAtBlock = this.isBoldRollupUserLogic(rollup) ? (await rollup.getAssertion(assertionId)).createdAtBlock : (await rollup.getNode(assertionId)).createdAtBlock; let createdFromBlock = createdAtBlock; let createdToBlock = createdAtBlock; // If L1 is Arbitrum, then L2 is an Orbit chain. if (await (0, lib_1.isArbitrumChain)(this.parentProvider)) { try { const nodeInterface = NodeInterface__factory_1.NodeInterface__factory.connect(constants_1.NODE_INTERFACE_ADDRESS, this.parentProvider); const l2BlockRangeFromNode = await nodeInterface.l2BlockRangeForL1(createdAtBlock); createdFromBlock = l2BlockRangeFromNode.firstBlock; createdToBlock = l2BlockRangeFromNode.lastBlock; } catch (_a) { // defaults to binary search try { const l2BlockRange = await getBlockRangesForL1BlockWithCache({ parentProvider: this.parentProvider, childProvider: childProvider, forL1Block: createdAtBlock.toNumber(), }); const startBlock = l2BlockRange[0]; const endBlock = l2BlockRange[1]; if (!startBlock || !endBlock) { throw new Error(); } createdFromBlock = bignumber_1.BigNumber.from(startBlock); createdToBlock = bignumber_1.BigNumber.from(endBlock); } catch (_b) { // fallback to the original method createdFromBlock = createdAtBlock; createdToBlock = createdAtBlock; } } } // now get the block hash and sendroot for that node const eventFetcher = new eventFetcher_1.EventFetcher(rollup.provider); const logs = this.isBoldRollupUserLogic(rollup) ? await eventFetcher.getEvents(BoldRollupUserLogic__factory_1.BoldRollupUserLogic__factory, t => t.filters.AssertionCreated(assertionId), { fromBlock: createdFromBlock.toNumber(), toBlock: createdToBlock.toNumber(), address: rollup.address, }) : await eventFetcher.getEvents(RollupUserLogic__factory_1.RollupUserLogic__factory, t => t.filters.NodeCreated(assertionId), { fromBlock: createdFromBlock.toNumber(), toBlock: createdToBlock.toNumber(), address: rollup.address, }); if (logs.length > 1) throw new errors_1.ArbSdkError(`Unexpected number of AssertionCreated events. Expected 0 or 1, got ${logs.length}.`); return await this.getBlockFromAssertionLog(childProvider, logs[0]); } async getBatchNumber(childProvider) { if (this.l1BatchNumber == undefined) { // findBatchContainingBlock errors if block number does not exist try { const nodeInterface = NodeInterface__factory_1.NodeInterface__factory.connect(constants_1.NODE_INTERFACE_ADDRESS, childProvider); const res = await nodeInterface.findBatchContainingBlock(this.event.arbBlockNum); this.l1BatchNumber = res.toNumber(); } catch (err) { // do nothing - errors are expected here } } return this.l1BatchNumber; } async getSendProps(childProvider) { if (!this.sendRootConfirmed) { const childChain = await (0, networks_1.getArbitrumNetwork)(childProvider); const rollup = await this.getRollupAndUpdateNetwork(childChain); const latestConfirmedAssertionId = await rollup.callStatic.latestConfirmed(); const childBlockConfirmed = await this.getBlockFromAssertionId(rollup, latestConfirmedAssertionId, childProvider); const sendRootSizeConfirmed = bignumber_1.BigNumber.from(childBlockConfirmed.sendCount); if (sendRootSizeConfirmed.gt(this.event.position)) { this.sendRootSize = sendRootSizeConfirmed; this.sendRootHash = childBlockConfirmed.sendRoot; this.sendRootConfirmed = true; } else { let latestCreatedAssertionId; if (this.isBoldRollupUserLogic(rollup)) { const latestConfirmed = await rollup.latestConfirmed(); const latestConfirmedAssertion = await rollup.getAssertion(latestConfirmed); const eventFetcher = new eventFetcher_1.EventFetcher(rollup.provider); const assertionCreatedEvents = await eventFetcher.getEvents(BoldRollupUserLogic__factory_1.BoldRollupUserLogic__factory, t => t.filters.AssertionCreated(), { fromBlock: latestConfirmedAssertion.createdAtBlock.toNumber(), toBlock: 'latest', address: rollup.address, }); if (assertionCreatedEvents.length !== 0) { latestCreatedAssertionId = assertionCreatedEvents[assertionCreatedEvents.length - 1].event .assertionHash; } else { latestCreatedAssertionId = latestConfirmedAssertionId; } } else { latestCreatedAssertionId = await rollup.callStatic.latestNodeCreated(); } const latestEquals = typeof latestCreatedAssertionId === 'string' ? latestCreatedAssertionId === latestConfirmedAssertionId : latestCreatedAssertionId.eq(latestConfirmedAssertionId); // if the node has yet to be confirmed we'll still try to find proof info from unconfirmed nodes if (!latestEquals) { // In rare case latestNodeNum can be equal to latestConfirmedNodeNum // eg immediately after an upgrade, or at genesis, or on a chain where confirmation time = 0 like AnyTrust may have const childBlock = await this.getBlockFromAssertionId(rollup, latestCreatedAssertionId, childProvider); const sendRootSize = bignumber_1.BigNumber.from(childBlock.sendCount); if (sendRootSize.gt(this.event.position)) { this.sendRootSize = sendRootSize; this.sendRootHash = childBlock.sendRoot; } } } } return { sendRootSize: this.sendRootSize, sendRootHash: this.sendRootHash, sendRootConfirmed: this.sendRootConfirmed, }; } /** * Waits until the outbox entry has been created, and will not return until it has been. * WARNING: Outbox entries are only created when the corresponding node is confirmed. Which * can take 1 week+, so waiting here could be a very long operation. * @param retryDelay * @returns outbox entry status (either executed or confirmed but not pending) */ async waitUntilReadyToExecute(childProvider, retryDelay = 500) { const status = await this.status(childProvider); if (status === message_1.ChildToParentMessageStatus.CONFIRMED || status === message_1.ChildToParentMessageStatus.EXECUTED) { return status; } else { await (0, lib_1.wait)(retryDelay); return await this.waitUntilReadyToExecute(childProvider, retryDelay); } } /** * Check whether the provided network has a BoLD rollup * @param arbitrumNetwork * @param parentProvider * @returns */ async isBold(arbitrumNetwork, parentProvider) { const bridge = Bridge__factory_1.Bridge__factory.connect(arbitrumNetwork.ethBridge.bridge, parentProvider); const remoteRollupAddr = await bridge.rollup(); const rollup = RollupUserLogic__factory_1.RollupUserLogic__factory.connect(remoteRollupAddr, parentProvider); try { // bold rollup does not have an extraChallengeTimeBlocks function await rollup.callStatic.extraChallengeTimeBlocks(); return undefined; } catch (err) { if (err instanceof Error && err.code === logger_1.Logger.errors.CALL_EXCEPTION) { return remoteRollupAddr; } throw err; } } /** * If the local network is not currently bold, checks if the remote network is bold * and if so updates the local network with a new rollup address * @param arbitrumNetwork * @returns The rollup contract, bold or legacy */ async getRollupAndUpdateNetwork(arbitrumNetwork) { if (!arbitrumNetwork.isBold) { const boldRollupAddr = await this.isBold(arbitrumNetwork, this.parentProvider); if (boldRollupAddr) { arbitrumNetwork.isBold = true; arbitrumNetwork.ethBridge.rollup = boldRollupAddr; } } return arbitrumNetwork.isBold ? BoldRollupUserLogic__factory_1.BoldRollupUserLogic__factory.connect(arbitrumNetwork.ethBridge.rollup, this.parentProvider) : RollupUserLogic__factory_1.RollupUserLogic__factory.connect(arbitrumNetwork.ethBridge.rollup, this.parentProvider); } /** * Estimates the L1 block number in which this L2 to L1 tx will be available for execution. * If the message can or already has been executed, this returns null * @param childProvider * @returns expected parent chain block number where the child chain to parent chain message will be executable. Returns null if the message can be or already has been executed */ async getFirstExecutableBlock(childProvider) { const arbitrumNetwork = await (0, networks_1.getArbitrumNetwork)(childProvider); const rollup = await this.getRollupAndUpdateNetwork(arbitrumNetwork); const status = await this.status(childProvider); if (status === message_1.ChildToParentMessageStatus.EXECUTED) return null; if (status === message_1.ChildToParentMessageStatus.CONFIRMED) return null; // consistency check in case we change the enum in the future if (status !== message_1.ChildToParentMessageStatus.UNCONFIRMED) throw new errors_1.ArbSdkError('ChildToParentMsg expected to be unconfirmed'); const latestBlock = await this.parentProvider.getBlockNumber(); const eventFetcher = new eventFetcher_1.EventFetcher(this.parentProvider); let logs; if (arbitrumNetwork.isBold) { logs = (await eventFetcher.getEvents(BoldRollupUserLogic__factory_1.BoldRollupUserLogic__factory, t => t.filters.AssertionCreated(), { fromBlock: Math.max(latestBlock - bignumber_1.BigNumber.from(arbitrumNetwork.confirmPeriodBlocks) .add(ASSERTION_CONFIRMED_PADDING) .toNumber(), 0), toBlock: 'latest', address: rollup.address, })).sort((a, b) => a.blockNumber - b.blockNumber); } else { logs = (await eventFetcher.getEvents(RollupUserLogic__factory_1.RollupUserLogic__factory, t => t.filters.NodeCreated(), { fromBlock: Math.max(latestBlock - bignumber_1.BigNumber.from(arbitrumNetwork.confirmPeriodBlocks) .add(ASSERTION_CONFIRMED_PADDING) .toNumber(), 0), toBlock: 'latest', address: rollup.address, })).sort((a, b) => a.event.nodeNum.toNumber() - b.event.nodeNum.toNumber()); } const lastChildBlock = logs.length === 0 ? undefined : await this.getBlockFromAssertionLog(childProvider, logs[logs.length - 1]); const lastSendCount = lastChildBlock ? bignumber_1.BigNumber.from(lastChildBlock.sendCount) : bignumber_1.BigNumber.from(0); // here we assume the child-to-parent tx is actually valid, so the user needs to wait the max time // since there isn't a pending assertion that includes this message yet if (lastSendCount.lte(this.event.position)) return bignumber_1.BigNumber.from(arbitrumNetwork.confirmPeriodBlocks) .add(ASSERTION_CREATED_PADDING) .add(ASSERTION_CONFIRMED_PADDING) .add(latestBlock); // use binary search to find the first assertion with sendCount > this.event.position // default to the last assertion since we already checked above let foundLog = logs[logs.length - 1]; let left = 0; let right = logs.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); const log = logs[mid]; const childBlock = await this.getBlockFromAssertionLog(childProvider, log); const sendCount = bignumber_1.BigNumber.from(childBlock.sendCount); if (sendCount.gt(this.event.position)) { foundLog = log; right = mid - 1; } else { left = mid + 1; } } if (arbitrumNetwork.isBold) { const assertionHash = foundLog .event.assertionHash; const assertion = await rollup.getAssertion(assertionHash); return assertion.createdAtBlock .add(arbitrumNetwork.confirmPeriodBlocks) .add(ASSERTION_CONFIRMED_PADDING); } else { const earliestNodeWithExit = foundLog .event.nodeNum; const node = await rollup.getNode(earliestNodeWithExit); return node.deadlineBlock.add(ASSERTION_CONFIRMED_PADDING); } } } exports.ChildToParentMessageReaderNitro = ChildToParentMessageReaderNitro; /** * Provides read and write access for nitro child-to-Parent-messages */ class ChildToParentMessageWriterNitro extends ChildToParentMessageReaderNitro { /** * Instantiates a new `ChildToParentMessageWriterNitro` object. * * @param {Signer} parentSigner The signer to be used for executing the Child-to-Parent message. * @param {EventArgs<ChildToParentTxEvent>} event The event containing the data of the Child-to-Parent message. * @param {Provider} [parentProvider] Optional. Used to override the Provider which is attached to `parentSigner` in case you need more control. This will be a required parameter in a future major version update. */ constructor(parentSigner, event, parentProvider) { super(parentProvider !== null && parentProvider !== void 0 ? parentProvider : parentSigner.provider, event); this.parentSigner = parentSigner; } /** * Executes the ChildToParentMessage on Parent Chain. * Will throw an error if the outbox entry has not been created, which happens when the * corresponding assertion is confirmed. * @returns */ async execute(childProvider, overrides) { const status = await this.status(childProvider); if (status !== message_1.ChildToParentMessageStatus.CONFIRMED) { throw new errors_1.ArbSdkError(`Cannot execute message. Status is: ${status} but must be ${message_1.ChildToParentMessageStatus.CONFIRMED}.`); } const proof = await this.getOutboxProof(childProvider); const childChain = await (0, networks_1.getArbitrumNetwork)(childProvider); const outbox = Outbox__factory_1.Outbox__factory.connect(childChain.ethBridge.outbox, this.parentSigner); return await outbox.executeTransaction(proof, this.event.position, this.event.caller, this.event.destination, this.event.arbBlockNum, this.event.ethBlockNum, this.event.timestamp, this.event.callvalue, this.event.data, overrides || {}); } } exports.ChildToParentMessageWriterNitro = ChildToParentMessageWriterNitro;