@arbitrum/sdk
Version:
Typescript library client-side interactions with Arbitrum
486 lines (485 loc) • 24.8 kB
JavaScript
/*
* 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 */
;
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;