Commit 414be841 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge branch 'develop' into inphi/proxyd-hdr

parents af40ffd8 1c6d89be
---
'@eth-optimism/core-utils': patch
---
Improved docstrings for BCFG typings
---
'@eth-optimism/integration-tests': minor
---
Updates to work with a live network
---
'@eth-optimism/batch-submitter-service': patch
---
Adds confirmation depth awareness to txmgr
---
'@eth-optimism/l2geth': patch
---
Add a better error message for when the sequencer url is not configured when proxying user requests to the sequencer for `eth_sendRawTransaction` when running as a verifier/replica
---
'@eth-optimism/proxyd': minor
---
Add caching for block-dependent RPCs
---
'@eth-optimism/proxyd': minor
---
proxyd: Cache block-dependent RPCs
---
'@eth-optimism/integration-tests': patch
---
Use hardhat-ethers for importing factories in integration tests
---
'@eth-optimism/l2geth': patch
---
Add reinitialize-by-url command, add dump chain state command
---
'@eth-optimism/core-utils': patch
---
Cleans up the internal file and folder structure for the typings exported by core-utils
---
'@eth-optimism/integration-tests': patch
---
Split OVMMulticall.sol into Multicall.sol & OVMContext.sol
---
'@eth-optimism/batch-submitter-service': minor
---
Add multi-tx support, clear pending txs on startup
---
'@eth-optimism/l2geth': patch
---
Fix blocknumber monotonicity logging bug
---
'@eth-optimism/proxyd': minor
---
Add integration tests and batching
# @eth-optimism/batch-submitter-service
## 0.1.0
### Minor Changes
- 356b7271: Add multi-tx support, clear pending txs on startup
### Patch Changes
- 85aa148d: Adds confirmation depth awareness to txmgr
## 0.0.2
### Patch Changes
- d6e0de5a: Fix metrics server
{
"name": "@eth-optimism/batch-submitter-service",
"version": "0.0.2",
"version": "0.1.0",
"private": true,
"devDependencies": {}
}
# @eth-optimism/proxyd
## 3.6.0
### Minor Changes
- 096c5f20: proxyd: Allow cached RPCs to be evicted by redis
- 71d64834: Add caching for block-dependent RPCs
- fd2e1523: proxyd: Cache block-dependent RPCs
- 1760613c: Add integration tests and batching
## 3.5.0
### Minor Changes
......
......@@ -2,6 +2,7 @@ package proxyd
import (
"context"
"time"
"github.com/go-redis/redis/v8"
"github.com/golang/snappy"
......@@ -16,6 +17,8 @@ type Cache interface {
const (
// assuming an average RPCRes size of 3 KB
memoryCacheLimit = 4096
// Set a large ttl to avoid expirations. However, a ttl must be set for volatile-lru to take effect.
redisTTL = 30 * 7 * 24 * time.Hour
)
type cache struct {
......@@ -67,7 +70,7 @@ func (c *redisCache) Get(ctx context.Context, key string) (string, error) {
}
func (c *redisCache) Put(ctx context.Context, key string, value string) error {
err := c.rdb.Set(ctx, key, value, 0).Err()
err := c.rdb.SetEX(ctx, key, value, redisTTL).Err()
if err != nil {
RecordRedisError("CacheSet")
}
......
{
"name": "@eth-optimism/proxyd",
"version": "3.5.0",
"version": "3.6.0",
"private": true,
"dependencies": {}
}
# @eth-optimism/integration-tests
## 0.5.0
### Minor Changes
- c1e923f9: Updates to work with a live network
### Patch Changes
- 968fb38d: Use hardhat-ethers for importing factories in integration tests
- a7fbafa8: Split OVMMulticall.sol into Multicall.sol & OVMContext.sol
## 0.4.2
### Patch Changes
......
{
"private": true,
"name": "@eth-optimism/integration-tests",
"version": "0.4.2",
"version": "0.5.0",
"description": "[Optimism] Integration tests",
"scripts": {
"lint": "yarn lint:fix && yarn lint:check",
......@@ -28,9 +28,9 @@
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"devDependencies": {
"@eth-optimism/contracts": "0.5.8",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/message-relayer": "0.2.12",
"@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.4",
"@eth-optimism/message-relayer": "0.2.13",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.4.5",
"@ethersproject/transactions": "^5.4.0",
......
# Changelog
## 0.5.8
### Patch Changes
- 949916f8: Add a better error message for when the sequencer url is not configured when proxying user requests to the sequencer for `eth_sendRawTransaction` when running as a verifier/replica
- 300f79bf: Fix nonce issue
- ae96d784: Add reinitialize-by-url command, add dump chain state command
- c7569a16: Fix blocknumber monotonicity logging bug
## 0.5.7
### Patch Changes
......
......@@ -208,14 +208,13 @@ func (st *StateTransition) buyGas() error {
func (st *StateTransition) preCheck() error {
// Make sure this transaction's nonce is correct.
if st.msg.CheckNonce() {
if rcfg.UsingOVM {
if st.msg.QueueOrigin() == types.QueueOriginL1ToL2 {
return st.buyGas()
}
}
nonce := st.state.GetNonce(st.msg.From())
if nonce < st.msg.Nonce() {
if rcfg.UsingOVM {
// The nonce never increments for L1ToL2 txs
if st.msg.QueueOrigin() == types.QueueOriginL1ToL2 {
return st.buyGas()
}
}
return ErrNonceTooHigh
} else if nonce > st.msg.Nonce() {
return ErrNonceTooLow
......
{
"name": "@eth-optimism/l2geth",
"version": "0.5.7",
"version": "0.5.8",
"private": true,
"devDependencies": {}
}
......@@ -32,6 +32,10 @@ docker-compose \
*Note*: This generates a large amount of log data which docker stores by default. See [Disk Usage](#disk-usage).
Also note that Docker Desktop only allocates 2GB of memory by default, which isn't enough to run the docker-compose services reliably.
To allocate more memory, go to Settings > Resources in the Docker UI and use the slider to change the value (_4GB recommended_). Make sure to click Apply & Restart for the changes to take effect.
To start the stack with monitoring enabled, just add the metric composition file.
```
docker-compose \
......
# Changelog
## 0.4.14
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.4.13
### Patch Changes
......
{
"private": true,
"name": "@eth-optimism/batch-submitter",
"version": "0.4.13",
"version": "0.4.14",
"description": "[Optimism] Service for submitting transactions and transaction results",
"main": "dist/index",
"types": "dist/index",
......@@ -34,8 +34,8 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.8",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.4",
"@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/providers": "^5.4.5",
......
# Changelog
## 0.5.9
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
## 0.5.8
### Patch Changes
......
{
"name": "@eth-optimism/contracts",
"version": "0.5.8",
"version": "0.5.9",
"description": "[Optimism] L1 and L2 smart contracts for Optimism",
"main": "dist/index",
"types": "dist/index",
......@@ -58,7 +58,7 @@
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/core-utils": "0.7.4",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/abstract-signer": "^5.4.1",
"@ethersproject/hardware-wallets": "^5.4.0"
......
# @eth-optimism/core-utils
## 0.7.4
### Patch Changes
- ba96a455: Improved docstrings for BCFG typings
- c3e85fef: Cleans up the internal file and folder structure for the typings exported by core-utils
## 0.7.3
### Patch Changes
......
{
"name": "@eth-optimism/core-utils",
"version": "0.7.3",
"version": "0.7.4",
"description": "[Optimism] Core typescript utilities",
"main": "dist/index",
"types": "dist/index",
......
# data transport layer
## 0.5.12
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.5.11
### Patch Changes
......
{
"private": true,
"name": "@eth-optimism/data-transport-layer",
"version": "0.5.11",
"version": "0.5.12",
"description": "[Optimism] Service for shuttling data from L1 into L2",
"main": "dist/index",
"types": "dist/index",
......@@ -37,8 +37,8 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.8",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.4",
"@ethersproject/providers": "^5.4.5",
"@ethersproject/transactions": "^5.4.0",
"@sentry/node": "^6.3.1",
......
# @eth-optimism/message-relayer
## 0.2.13
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.2.12
### Patch Changes
......
{
"name": "@eth-optimism/message-relayer",
"version": "0.2.12",
"version": "0.2.13",
"description": "[Optimism] Service for automatically relaying L2 to L1 transactions",
"main": "dist/index",
"types": "dist/index",
......@@ -35,8 +35,8 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.8",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.4",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.6",
"dotenv": "^10.0.0",
......
......@@ -32,7 +32,7 @@
},
"devDependencies": {
"@discoveryjs/json-ext": "^0.5.3",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/core-utils": "0.7.4",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abi": "^5.5.0",
"@ethersproject/bignumber": "^5.5.0",
......@@ -62,7 +62,7 @@
"ethers": "^5.4.5",
"lint-staged": "11.0.0",
"mocha": "^9.1.2",
"node-fetch": "2.6.5",
"node-fetch": "2.6.7",
"solc": "0.8.7-fixed",
"ts-mocha": "^8.0.0",
"ts-node": "^10.0.0"
......
# @eth-optimism/replica-healthcheck
## 0.3.4
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
## 0.3.3
### Patch Changes
......
{
"private": true,
"name": "@eth-optimism/replica-healthcheck",
"version": "0.3.3",
"version": "0.3.4",
"description": "[Optimism] Service for monitoring the health of replica nodes",
"main": "dist/index",
"types": "dist/index",
......@@ -33,7 +33,7 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/core-utils": "0.7.4",
"dotenv": "^10.0.0",
"ethers": "^5.4.5",
"express": "^4.17.1",
......
# @eth-optimism/sdk
## 0.0.5
### Patch Changes
- Updated dependencies [ba96a455]
- Updated dependencies [c3e85fef]
- @eth-optimism/core-utils@0.7.4
- @eth-optimism/contracts@0.5.9
## 0.0.4
### Patch Changes
......
{
"name": "@eth-optimism/sdk",
"version": "0.0.4",
"version": "0.0.5",
"description": "[Optimism] Tools for working with Optimism",
"main": "dist/index",
"types": "dist/index",
......@@ -59,8 +59,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@eth-optimism/contracts": "0.5.8",
"@eth-optimism/core-utils": "0.7.3",
"@eth-optimism/contracts": "0.5.9",
"@eth-optimism/core-utils": "0.7.4",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abstract-signer": "^5.5.0",
"ethers": "^5.5.2"
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
L1ToL2Overrides,
MessageLike,
NumberLike,
MessageDirection,
} from './interfaces'
export class CrossChainMessenger implements ICrossChainMessenger {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
/**
* Creates a new CrossChainMessenger instance.
*
* @param opts Options for the messenger.
* @param opts.provider CrossChainProvider to use to send messages.
* @param opts.l1Signer Signer to use to send messages on L1.
* @param opts.l2Signer Signer to use to send messages on L2.
*/
constructor(opts: {
provider: ICrossChainProvider
l1Signer: Signer
l2Signer: Signer
}) {
this.provider = opts.provider
this.l1Signer = opts.l1Signer
this.l2Signer = opts.l2Signer
}
public async sendMessage(
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.sendMessage(message, overrides)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.l1Signer.sendTransaction(tx)
} else {
return this.l2Signer.sendTransaction(tx)
}
}
public async resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async finalizeMessage(
message: MessageLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async depositETH(
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async withdrawETH(
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
populateTransaction = {
sendMessage: async (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.sendMessage(
message.target,
message.message,
overrides?.l2GasLimit ||
(await this.provider.estimateL2MessageGasLimit(message))
)
} else {
return this.provider.contracts.l2.L2CrossDomainMessenger.connect(
this.l2Signer
).populateTransaction.sendMessage(
message.target,
message.message,
0 // Gas limit goes unused when sending from L2 to L1
)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
finalizeMessage: async (
message: MessageLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
withdrawETH: async (
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
}
estimateGas = {
sendMessage: async (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
): Promise<BigNumber> => {
const tx = await this.populateTransaction.sendMessage(message, overrides)
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.l1Provider.estimateGas(tx)
} else {
return this.provider.l2Provider.estimateGas(tx)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
finalizeMessage: async (
message: MessageLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
withdrawETH: async (
amount: NumberLike,
overrides?: Overrides
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
}
}
......@@ -12,11 +12,13 @@ import {
OEContracts,
OEContractsLike,
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
ProviderLike,
CrossChainMessage,
CrossChainMessageRequest,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
......@@ -24,6 +26,8 @@ import {
MessageReceiptStatus,
CustomBridges,
CustomBridgesLike,
StateRoot,
StateRootBatch,
} from './interfaces'
import {
toProvider,
......@@ -50,6 +54,7 @@ export class CrossChainProvider implements ICrossChainProvider {
* @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/
constructor(opts: {
l1Provider: ProviderLike
......@@ -336,7 +341,44 @@ export class CrossChainProvider implements ICrossChainProvider {
}
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
throw new Error('Not implemented')
const resolved = await this.toCrossChainMessage(message)
const receipt = await this.getMessageReceipt(resolved)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (receipt === null) {
return MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.FAILED_L1_TO_L2_MESSAGE
}
}
} else {
if (receipt === null) {
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
}
}
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
} else {
return MessageStatus.READY_FOR_RELAY
}
}
}
}
public async getMessageReceipt(
......@@ -421,12 +463,21 @@ export class CrossChainProvider implements ICrossChainProvider {
}
public async estimateL2MessageGasLimit(
message: MessageLike,
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber> {
const resolved = await this.toCrossChainMessage(message)
let resolved: CrossChainMessage | CrossChainMessageRequest
let from: string
if ((message as CrossChainMessage).messageNonce === undefined) {
resolved = message as CrossChainMessageRequest
from = opts?.from
} else {
resolved = await this.toCrossChainMessage(message as MessageLike)
from = opts?.from || (resolved as CrossChainMessage).sender
}
// L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
......@@ -434,7 +485,7 @@ export class CrossChainProvider implements ICrossChainProvider {
}
const estimate = await this.l2Provider.estimateGas({
from: resolved.sender,
from,
to: resolved.target,
data: resolved.message,
})
......@@ -450,9 +501,162 @@ export class CrossChainProvider implements ICrossChainProvider {
throw new Error('Not implemented')
}
public async estimateMessageWaitTimeBlocks(
public async getChallengePeriodSeconds(): Promise<number> {
const challengePeriod =
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
return challengePeriod.toNumber()
}
public async getMessageStateRoot(
message: MessageLike
): Promise<number> {
throw new Error('Not implemented')
): Promise<StateRoot | null> {
const resolved = await this.toCrossChainMessage(message)
// State roots are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
// We need the block number of the transaction that triggered the message so we can look up the
// state root batch that corresponds to that block number.
const messageTxReceipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
// Every block has exactly one transaction in it. Since there's a genesis block, the
// transaction index will always be one less than the block number.
const messageTxIndex = messageTxReceipt.blockNumber - 1
// Pull down the state root batch, we'll try to pick out the specific state root that
// corresponds to our message.
const stateRootBatch = await this.getStateRootBatchByTransactionIndex(
messageTxIndex
)
// No state root batch, no state root.
if (stateRootBatch === null) {
return null
}
// We have a state root batch, now we need to find the specific state root for our transaction.
// First we need to figure out the index of the state root within the batch we found. This is
// going to be the original transaction index offset by the total number of previous state
// roots.
const indexInBatch =
messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber()
// Just a sanity check.
if (stateRootBatch.stateRoots.length <= indexInBatch) {
// Should never happen!
throw new Error(`state root does not exist in batch`)
}
return {
blockNumber: stateRootBatch.blockNumber,
header: stateRootBatch.header,
stateRoot: stateRootBatch.stateRoots[indexInBatch],
}
}
public async getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<ethers.Event | null> {
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) {
// Should never happen!
throw new Error(`found more than one StateBatchAppended event`)
} else {
return events[0]
}
}
public async getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<ethers.Event | null> {
const isEventHi = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
return index < prevTotalElements
}
const isEventLo = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
const batchSize = event.args._batchSize.toNumber()
return index >= prevTotalElements + batchSize
}
const totalBatches: ethers.BigNumber =
await this.contracts.l1.StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound)
if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
} else if (!isEventHi(batchEvent, transactionIndex)) {
// Upper bound is not too low and also not too high. This means the upper bound event is the
// one we're looking for! Return it.
return batchEvent
}
// Binary search to find the right event. The above checks will guarantee that the event does
// exist and that we'll find it during this search.
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
}
public async getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null> {
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,
},
}
}
}
export * from './interfaces'
export * from './utils'
export * from './cross-chain-provider'
export * from './cross-chain-messenger'
import { Overrides, Signer } from 'ethers'
import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
......@@ -22,9 +22,14 @@ export interface ICrossChainMessenger {
provider: ICrossChainProvider
/**
* Signer that will carry out L1/L2 transactions.
* Signer that will carry out L1 transactions.
*/
signer: Signer
l1Signer: Signer
/**
* Signer that will carry out L2 transactions.
*/
l2Signer: Signer
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
......@@ -107,7 +112,7 @@ export interface ICrossChainMessenger {
sendMessage: (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
) => Promise<TransactionResponse>
) => Promise<TransactionRequest>
/**
* Generates a transaction that resends a given cross chain message. Only applies to L1 to L2
......@@ -178,7 +183,7 @@ export interface ICrossChainMessenger {
sendMessage: (
message: CrossChainMessageRequest,
overrides?: L1ToL2Overrides
) => Promise<TransactionResponse>
) => Promise<BigNumber>
/**
* Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages.
......@@ -192,7 +197,7 @@ export interface ICrossChainMessenger {
message: MessageLike,
messageGasLimit: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest>
): Promise<BigNumber>
/**
* Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages.
......@@ -204,7 +209,7 @@ export interface ICrossChainMessenger {
finalizeMessage(
message: MessageLike,
overrides?: Overrides
): Promise<TransactionRequest>
): Promise<BigNumber>
/**
* Estimates gas required to deposit some ETH into the L2 chain.
......@@ -216,7 +221,7 @@ export interface ICrossChainMessenger {
depositETH(
amount: NumberLike,
overrides?: L1ToL2Overrides
): Promise<TransactionRequest>
): Promise<BigNumber>
/**
* Estimates gas required to withdraw some ETH back to the L1 chain.
......@@ -225,9 +230,6 @@ export interface ICrossChainMessenger {
* @param overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdrawETH(
amount: NumberLike,
overrides?: Overrides
): Promise<TransactionRequest>
withdrawETH(amount: NumberLike, overrides?: Overrides): Promise<BigNumber>
}
}
import { BigNumber } from 'ethers'
import { Event, BigNumber } from 'ethers'
import { Provider, BlockTag } from '@ethersproject/abstract-provider'
import {
MessageLike,
MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
......@@ -13,6 +14,8 @@ import {
OEContracts,
MessageReceipt,
CustomBridges,
StateRoot,
StateRootBatch,
} from './types'
/**
......@@ -203,12 +206,14 @@ export interface ICrossChainProvider {
* @param message Message get a gas estimate for.
* @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
* @param opts.from Address to use as the sender.
* @returns Estimates L2 gas limit.
*/
estimateL2MessageGasLimit(
message: MessageLike,
message: MessageRequestLike,
opts?: {
bufferPercent?: number
from?: string
}
): Promise<BigNumber>
......@@ -224,13 +229,52 @@ export interface ICrossChainProvider {
estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number>
/**
* Returns the estimated amount of time before the message can be executed (in L1 blocks).
* When this is a message being sent to L1, this will return the estimated time until the message
* will complete its challenge period. When this is a message being sent to L2, this will return
* the estimated amount of time until the message will be picked up and executed on L2.
* Queries the current challenge period in seconds from the StateCommitmentChain.
*
* @param message Message to estimate the time remaining for.
* @returns Estimated amount of time remaining (in blocks) before the message can be executed.
* @returns Current challenge period in seconds.
*/
getChallengePeriodSeconds(): Promise<number>
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
* state root for the given message has not been published yet, this function returns null.
*
* @param message Message to find a state root for.
* @returns State root for the block in which the message was created.
*/
getMessageStateRoot(message: MessageLike): Promise<StateRoot | null>
/**
* Returns the StateBatchAppended event that was emitted when the batch with a given index was
* created. Returns null if no such event exists (the batch has not been submitted).
*
* @param batchIndex Index of the batch to find an event for.
* @returns StateBatchAppended event for the batch, or null if no such batch exists.
*/
getStateBatchAppendedEventByBatchIndex(
batchIndex: number
): Promise<Event | null>
/**
* Returns the StateBatchAppended event for the batch that includes the transaction with the
* given index. Returns null if no such event exists.
*
* @param transactionIndex Index of the L2 transaction to find an event for.
* @returns StateBatchAppended event for the batch that includes the given transaction by index.
*/
getStateBatchAppendedEventByTransactionIndex(
transactionIndex: number
): Promise<Event | null>
/**
* Returns information about the state root batch that included the state root for the given
* transaction by index. Returns null if no such state root has been published yet.
*
* @param transactionIndex Index of the L2 transaction to find a state root batch for.
* @returns State root batch for the given transaction index, or null if none exists yet.
*/
estimateMessageWaitTimeBlocks(message: MessageLike): Promise<number>
getStateRootBatchByTransactionIndex(
transactionIndex: number
): Promise<StateRootBatch | null>
}
......@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest {
direction: MessageDirection
target: string
message: string
l2GasLimit: NumberLike
}
/**
......@@ -212,9 +211,19 @@ export interface StateRootBatchHeader {
}
/**
* State root batch, including header and actual state roots.
* Information about a state root, including header, block number, and root iself.
*/
export interface StateRoot {
blockNumber: number
header: StateRootBatchHeader
stateRoot: string
}
/**
* Information about a batch of state roots.
*/
export interface StateRootBatch {
blockNumber: number
header: StateRootBatchHeader
stateRoots: string[]
}
......@@ -225,7 +234,7 @@ export interface StateRootBatch {
* limit field (gas used depends on the amount of gas provided).
*/
export type L1ToL2Overrides = Overrides & {
l2GasLimit: NumberLike
l2GasLimit?: NumberLike
}
/**
......@@ -234,13 +243,22 @@ export type L1ToL2Overrides = Overrides & {
export type TransactionLike = string | TransactionReceipt | TransactionResponse
/**
* Stuff that can be coerced into a message.
* Stuff that can be coerced into a CrossChainMessage.
*/
export type MessageLike =
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
/**
* Stuff that can be coerced into a CrossChainMessageRequest.
*/
export type MessageRequestLike =
| CrossChainMessageRequest
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
/**
* Stuff that can be coerced into a provider.
*/
......
......@@ -7,13 +7,22 @@ contract MockMessenger is ICrossDomainMessenger {
return address(0);
}
uint256 public nonce;
// Empty function to satisfy the interface.
function sendMessage(
address _target,
bytes calldata _message,
uint32 _gasLimit
) public {
return;
emit SentMessage(
_target,
msg.sender,
_message,
nonce,
_gasLimit
);
nonce++;
}
struct SentMessageEventParams {
......
pragma solidity ^0.8.9;
contract MockSCC {
event StateBatchAppended(
uint256 indexed _batchIndex,
bytes32 _batchRoot,
uint256 _batchSize,
uint256 _prevTotalElements,
bytes _extraData
);
struct StateBatchAppendedArgs {
uint256 batchIndex;
bytes32 batchRoot;
uint256 batchSize;
uint256 prevTotalElements;
bytes extraData;
}
// Window in seconds, will resolve to 100 blocks.
uint256 public FRAUD_PROOF_WINDOW = 1500;
uint256 public batches = 0;
StateBatchAppendedArgs public sbaParams;
function getTotalBatches() public view returns (uint256) {
return batches;
}
function setSBAParams(
StateBatchAppendedArgs memory _args
) public {
sbaParams = _args;
}
function appendStateBatch(
bytes32[] memory _roots,
uint256 _shouldStartAtIndex
) public {
batches++;
emit StateBatchAppended(
sbaParams.batchIndex,
sbaParams.batchRoot,
sbaParams.batchSize,
sbaParams.prevTotalElements,
sbaParams.extraData
);
}
}
import './setup'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
import { expect } from './setup'
import {
CrossChainProvider,
CrossChainMessenger,
MessageDirection,
} from '../src'
describe('CrossChainMessenger', () => {
let l1Signer: any
let l2Signer: any
before(async () => {
;[l1Signer, l2Signer] = await ethers.getSigners()
})
describe('sendMessage', () => {
describe('when no l2GasLimit is provided', () => {
it('should send a message with an estimated l2GasLimit')
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
describe('when the message is an L1 to L2 message', () => {
describe('when no l2GasLimit is provided', () => {
it('should send a message with an estimated l2GasLimit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
const estimate = await provider.estimateL2MessageGasLimit(message)
await expect(messenger.sendMessage(message))
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
0,
estimate
)
})
})
describe('when an l2GasLimit is provided', () => {
it('should send a message with the provided l2GasLimit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
await expect(
messenger.sendMessage(message, {
l2GasLimit: 1234,
})
)
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
0,
1234
)
})
})
})
describe('when an l2GasLimit is provided', () => {
it('should send a message with the provided l2GasLimit')
describe('when the message is an L2 to L1 message', () => {
it('should send a message', async () => {
const message = {
direction: MessageDirection.L2_TO_L1,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
await expect(messenger.sendMessage(message))
.to.emit(l2Messenger, 'SentMessage')
.withArgs(
message.target,
await l2Signer.getAddress(),
message.message,
0,
0
)
})
})
})
......
......@@ -10,7 +10,10 @@ import {
CONTRACT_ADDRESSES,
hashCrossChainMessage,
omit,
MessageStatus,
CrossChainMessage,
} from '../src'
import { DUMMY_MESSAGE } from './helpers'
describe('CrossChainProvider', () => {
describe('construction', () => {
......@@ -252,13 +255,7 @@ describe('CrossChainProvider', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
......@@ -311,13 +308,7 @@ describe('CrossChainProvider', () => {
for (const n of [1, 2, 4, 8]) {
it(`should find ${n} messages when the transaction emits ${n} messages`, async () => {
const messages = [...Array(n)].map(() => {
return {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
......@@ -738,15 +729,7 @@ describe('CrossChainProvider', () => {
describe('when the input is a TransactionLike', () => {
describe('when the transaction sent exactly one message', () => {
it('should return the CrossChainMessage sent in the transaction', async () => {
const message = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
const tx = await l1Messenger.triggerSentMessageEvents([message])
const tx = await l1Messenger.triggerSentMessageEvents([DUMMY_MESSAGE])
const foundCrossChainMessages =
await provider.getMessagesByTransaction(tx)
const resolved = await provider.toCrossChainMessage(tx)
......@@ -757,13 +740,7 @@ describe('CrossChainProvider', () => {
describe('when the transaction sent more than one message', () => {
it('should throw an error', async () => {
const messages = [...Array(2)].map(() => {
return {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
......@@ -785,45 +762,200 @@ describe('CrossChainProvider', () => {
})
describe('getMessageStatus', () => {
let scc: Contract
let l1Messenger: Contract
let l2Messenger: Contract
let provider: CrossChainProvider
beforeEach(async () => {
// TODO: Get rid of the nested awaits here. Could be a good first issue for someone.
scc = (await (await ethers.getContractFactory('MockSCC')).deploy()) as any
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
StateCommitmentChain: scc.address,
},
l2: {
L2CrossDomainMessenger: l2Messenger.address,
},
},
})
})
const sendAndGetDummyMessage = async (direction: MessageDirection) => {
const messenger =
direction === MessageDirection.L1_TO_L2 ? l1Messenger : l2Messenger
const tx = await messenger.triggerSentMessageEvents([DUMMY_MESSAGE])
return (
await provider.getMessagesByTransaction(tx, {
direction,
})
)[0]
}
const submitStateRootBatchForMessage = async (
message: CrossChainMessage
) => {
await scc.setSBAParams({
batchIndex: 0,
batchRoot: ethers.constants.HashZero,
batchSize: 1,
prevTotalElements: message.blockNumber,
extraData: '0x',
})
await scc.appendStateBatch([ethers.constants.HashZero], 0)
}
describe('when the message is an L1 => L2 message', () => {
describe('when the message has not been executed on L2 yet', () => {
it('should return a status of UNCONFIRMED_L1_TO_L2_MESSAGE')
it('should return a status of UNCONFIRMED_L1_TO_L2_MESSAGE', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
)
})
})
describe('when the message has been executed on L2', () => {
it('should return a status of RELAYED')
it('should return a status of RELAYED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.RELAYED
)
})
})
describe('when the message has been executed but failed', () => {
it('should return a status of FAILED_L1_TO_L2_MESSAGE')
it('should return a status of FAILED_L1_TO_L2_MESSAGE', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.FAILED_L1_TO_L2_MESSAGE
)
})
})
})
describe('when the message is an L2 => L1 message', () => {
describe('when the message state root has not been published', () => {
it('should return a status of STATE_ROOT_NOT_PUBLISHED')
it('should return a status of STATE_ROOT_NOT_PUBLISHED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.STATE_ROOT_NOT_PUBLISHED
)
})
})
describe('when the message state root is still in the challenge period', () => {
it('should return a status of IN_CHALLENGE_PERIOD')
it('should return a status of IN_CHALLENGE_PERIOD', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.IN_CHALLENGE_PERIOD
)
})
})
describe('when the message is no longer in the challenge period', () => {
describe('when the message has been relayed successfully', () => {
it('should return a status of RELAYED')
it('should return a status of RELAYED', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.RELAYED
)
})
})
describe('when the message has been relayed but the relay failed', () => {
it('should return a status of READY_FOR_RELAY')
it('should return a status of READY_FOR_RELAY', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
)
})
})
describe('when the message has not been relayed', () => {
it('should return a status of READY_FOR_RELAY')
it('should return a status of READY_FOR_RELAY', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
await submitStateRootBatchForMessage(message)
const challengePeriod = await provider.getChallengePeriodSeconds()
ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
ethers.provider.send('evm_mine', [])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
)
})
})
})
})
describe('when the message does not exist', () => {
// TODO: Figure out if this is the correct behavior. Mark suggests perhaps returning null.
it('should throw an error')
})
})
......
export const DUMMY_MESSAGE = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
}
export * from './constants'
......@@ -11523,15 +11523,15 @@ node-fetch@*:
data-uri-to-buffer "^3.0.1"
fetch-blob "^3.1.2"
node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1:
node-fetch@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment