Commit 70810a49 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2096 from ethereum-optimism/develop

Develop -> Master PR
parents 3a79a2d5 cdc4ddd8
---
'@eth-optimism/core-utils': patch
---
test/docs: Improve docstrings and tests for utils inside of hex-strings.ts
---
'@eth-optimism/batch-submitter': patch
---
Adds a fix for the BSS to account for new timestamp logic in L2Geth
---
'@eth-optimism/proxyd': minor
---
Add debug cache status header to proxyd responses
---
'@eth-optimism/batch-submitter': patch
---
Import performance to not couple batch submitter to version of nodejs that has performance as a builtin
...@@ -65,7 +65,7 @@ jobs: ...@@ -65,7 +65,7 @@ jobs:
run: yarn changeset version --snapshot run: yarn changeset version --snapshot
- name: Publish To NPM - name: Publish To NPM
uses: changesets/action@master uses: changesets/action@v1
id: changesets id: changesets
with: with:
publish: yarn changeset publish --tag canary publish: yarn changeset publish --tag canary
......
...@@ -55,7 +55,7 @@ jobs: ...@@ -55,7 +55,7 @@ jobs:
run: yarn run: yarn
- name: Publish To NPM or Create Release Pull Request - name: Publish To NPM or Create Release Pull Request
uses: changesets/action@master uses: changesets/action@v1
id: changesets id: changesets
with: with:
publish: yarn release publish: yarn release
...@@ -101,14 +101,6 @@ jobs: ...@@ -101,14 +101,6 @@ jobs:
push: true push: true
tags: ethereumoptimism/l2geth:${{ needs.release.outputs.l2geth }},ethereumoptimism/l2geth:latest tags: ethereumoptimism/l2geth:${{ needs.release.outputs.l2geth }},ethereumoptimism/l2geth:latest
- name: Publish rpc-proxy
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.rpc-proxy
push: true
tags: ethereumoptimism/rpc-proxy:${{ needs.release.outputs.l2geth }},ethereumoptimism/rpc-proxy:latest
gas-oracle: gas-oracle:
name: Publish Gas Oracle Version ${{ needs.release.outputs.gas-oracle }} name: Publish Gas Oracle Version ${{ needs.release.outputs.gas-oracle }}
needs: release needs: release
......
...@@ -25,6 +25,7 @@ const ( ...@@ -25,6 +25,7 @@ const (
ContextKeyReqID = "req_id" ContextKeyReqID = "req_id"
ContextKeyXForwardedFor = "x_forwarded_for" ContextKeyXForwardedFor = "x_forwarded_for"
MaxBatchRPCCalls = 100 MaxBatchRPCCalls = 100
cacheStatusHdr = "X-Proxyd-Cache-Status"
) )
type Server struct { type Server struct {
...@@ -159,6 +160,7 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { ...@@ -159,6 +160,7 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
} }
batchRes := make([]*RPCRes, len(reqs), len(reqs)) batchRes := make([]*RPCRes, len(reqs), len(reqs))
var batchContainsCached bool
for i := 0; i < len(reqs); i++ { for i := 0; i < len(reqs); i++ {
req, err := ParseRPCReq(reqs[i]) req, err := ParseRPCReq(reqs[i])
if err != nil { if err != nil {
...@@ -167,9 +169,14 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { ...@@ -167,9 +169,14 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
continue continue
} }
batchRes[i] = s.handleSingleRPC(ctx, req) var cached bool
batchRes[i], cached = s.handleSingleRPC(ctx, req)
if cached {
batchContainsCached = true
}
} }
setCacheHeader(w, batchContainsCached)
writeBatchRPCRes(ctx, w, batchRes) writeBatchRPCRes(ctx, w, batchRes)
return return
} }
...@@ -181,14 +188,15 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { ...@@ -181,14 +188,15 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
return return
} }
backendRes := s.handleSingleRPC(ctx, req) backendRes, cached := s.handleSingleRPC(ctx, req)
setCacheHeader(w, cached)
writeRPCRes(ctx, w, backendRes) writeRPCRes(ctx, w, backendRes)
} }
func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) (*RPCRes, bool) {
if err := ValidateRPCReq(req); err != nil { if err := ValidateRPCReq(req); err != nil {
RecordRPCError(ctx, BackendProxyd, MethodUnknown, err) RecordRPCError(ctx, BackendProxyd, MethodUnknown, err)
return NewRPCErrorRes(nil, err) return NewRPCErrorRes(nil, err), false
} }
group := s.rpcMethodMappings[req.Method] group := s.rpcMethodMappings[req.Method]
...@@ -202,7 +210,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { ...@@ -202,7 +210,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
"method", req.Method, "method", req.Method,
) )
RecordRPCError(ctx, BackendProxyd, MethodUnknown, ErrMethodNotWhitelisted) RecordRPCError(ctx, BackendProxyd, MethodUnknown, ErrMethodNotWhitelisted)
return NewRPCErrorRes(req.ID, ErrMethodNotWhitelisted) return NewRPCErrorRes(req.ID, ErrMethodNotWhitelisted), false
} }
var backendRes *RPCRes var backendRes *RPCRes
...@@ -215,7 +223,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { ...@@ -215,7 +223,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
) )
} }
if backendRes != nil { if backendRes != nil {
return backendRes return backendRes, true
} }
backendRes, err = s.backendGroups[group].Forward(ctx, req) backendRes, err = s.backendGroups[group].Forward(ctx, req)
...@@ -226,7 +234,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { ...@@ -226,7 +234,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
"req_id", GetReqID(ctx), "req_id", GetReqID(ctx),
"err", err, "err", err,
) )
return NewRPCErrorRes(req.ID, err) return NewRPCErrorRes(req.ID, err), false
} }
if backendRes.Error == nil { if backendRes.Error == nil {
...@@ -239,7 +247,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { ...@@ -239,7 +247,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
} }
} }
return backendRes return backendRes, false
} }
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
...@@ -322,6 +330,14 @@ func (s *Server) populateContext(w http.ResponseWriter, r *http.Request) context ...@@ -322,6 +330,14 @@ func (s *Server) populateContext(w http.ResponseWriter, r *http.Request) context
) )
} }
func setCacheHeader(w http.ResponseWriter, cached bool) {
if cached {
w.Header().Set(cacheStatusHdr, "HIT")
} else {
w.Header().Set(cacheStatusHdr, "MISS")
}
}
func writeRPCError(ctx context.Context, w http.ResponseWriter, id json.RawMessage, err error) { func writeRPCError(ctx context.Context, w http.ResponseWriter, id json.RawMessage, err error) {
var res *RPCRes var res *RPCRes
if r, ok := err.(*RPCErr); ok { if r, ok := err.(*RPCErr); ok {
......
/* External Imports */ /* External Imports */
import { performance } from 'perf_hooks'
import { Promise as bPromise } from 'bluebird' import { Promise as bPromise } from 'bluebird'
import { Contract, Signer, providers } from 'ethers' import { Contract, Signer, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider' import { TransactionReceipt } from '@ethersproject/abstract-provider'
......
/* External Imports */ /* External Imports */
import { performance } from 'perf_hooks'
import { Promise as bPromise } from 'bluebird' import { Promise as bPromise } from 'bluebird'
import { Signer, ethers, Contract, providers } from 'ethers' import { Signer, ethers, Contract, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider' import { TransactionReceipt } from '@ethersproject/abstract-provider'
...@@ -683,10 +685,18 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -683,10 +685,18 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
queued: BatchElement[] queued: BatchElement[]
}> = [] }> = []
for (const block of blocks) { for (const block of blocks) {
// Create a new context in certain situations
if ( if (
(lastBlockIsSequencerTx === false && block.isSequencerTx === true) || // If there are no contexts yet, create a new context.
groupedBlocks.length === 0 || groupedBlocks.length === 0 ||
(block.timestamp !== lastTimestamp && block.isSequencerTx === true) || // If the last block was an L1 to L2 transaction, but the next block is a Sequencer
// transaction, create a new context.
(lastBlockIsSequencerTx === false && block.isSequencerTx === true) ||
// If the timestamp of the last block differs from the timestamp of the current block,
// create a new context. Applies to both L1 to L2 transactions and Sequencer transactions.
block.timestamp !== lastTimestamp ||
// If the block number of the last block differs from the block number of the current block,
// create a new context. ONLY applies to Sequencer transactions.
(block.blockNumber !== lastBlockNumber && block.isSequencerTx === true) (block.blockNumber !== lastBlockNumber && block.isSequencerTx === true)
) { ) {
groupedBlocks.push({ groupedBlocks.push({
...@@ -694,6 +704,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -694,6 +704,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
queued: [], queued: [],
}) })
} }
const cur = groupedBlocks.length - 1 const cur = groupedBlocks.length - 1
block.isSequencerTx block.isSequencerTx
? groupedBlocks[cur].sequenced.push(block) ? groupedBlocks[cur].sequenced.push(block)
......
...@@ -9,8 +9,6 @@ Within each contract file you'll find a comment that lists: ...@@ -9,8 +9,6 @@ Within each contract file you'll find a comment that lists:
1. The compiler with which a contract is intended to be compiled, `solc` or `optimistic-solc`. 1. The compiler with which a contract is intended to be compiled, `solc` or `optimistic-solc`.
2. The network upon to which the contract will be deployed, `OVM` or `EVM`. 2. The network upon to which the contract will be deployed, `OVM` or `EVM`.
A more detailed overview of these contracts can be found on the [community hub](http://community.optimism.io/docs/protocol/protocol.html#system-overview).
<!-- TODO: Add link to final contract docs here when finished. --> <!-- TODO: Add link to final contract docs here when finished. -->
## Usage (npm) ## Usage (npm)
......
...@@ -56,6 +56,12 @@ export const toHexString = (inp: Buffer | string | number | null): string => { ...@@ -56,6 +56,12 @@ export const toHexString = (inp: Buffer | string | number | null): string => {
} }
} }
/**
* Casts a number to a hex string without zero padding.
*
* @param n Number to cast to a hex string.
* @return Number cast as a hex string.
*/
export const toRpcHexString = (n: number | BigNumber): string => { export const toRpcHexString = (n: number | BigNumber): string => {
let num let num
if (typeof n === 'number') { if (typeof n === 'number') {
...@@ -67,10 +73,18 @@ export const toRpcHexString = (n: number | BigNumber): string => { ...@@ -67,10 +73,18 @@ export const toRpcHexString = (n: number | BigNumber): string => {
if (num === '0x0') { if (num === '0x0') {
return num return num
} else { } else {
// BigNumber pads a single 0 to keep hex length even
return num.replace(/^0x0/, '0x') return num.replace(/^0x0/, '0x')
} }
} }
/**
* Zero pads a hex string if str.length !== 2 + length * 2. Pads to length * 2.
*
* @param str Hex string to pad
* @param length Half the length of the desired padded hex string
* @return Hex string with length of 2 + length * 2
*/
export const padHexString = (str: string, length: number): string => { export const padHexString = (str: string, length: number): string => {
if (str.length === 2 + length * 2) { if (str.length === 2 + length * 2) {
return str return str
...@@ -79,9 +93,25 @@ export const padHexString = (str: string, length: number): string => { ...@@ -79,9 +93,25 @@ export const padHexString = (str: string, length: number): string => {
} }
} }
export const encodeHex = (val: any, len: number) => /**
* Casts an input to hex string without '0x' prefix with conditional padding.
* Hex string will always start with a 0.
*
* @param val Input to cast to a hex string.
* @param len Desired length to pad hex string. Ignored if less than hex string length.
* @return Hex string with '0' prefix
*/
export const encodeHex = (val: any, len: number): string =>
remove0x(BigNumber.from(val).toHexString()).padStart(len, '0') remove0x(BigNumber.from(val).toHexString()).padStart(len, '0')
/**
* Case insensitive hex string equality check
*
* @param stringA Hex string A
* @param stringB Hex string B
* @throws {Error} Inputs must be valid hex strings
* @return True if equal
*/
export const hexStringEquals = (stringA: string, stringB: string): boolean => { export const hexStringEquals = (stringA: string, stringB: string): boolean => {
if (!ethers.utils.isHexString(stringA)) { if (!ethers.utils.isHexString(stringA)) {
throw new Error(`input is not a hex string: ${stringA}`) throw new Error(`input is not a hex string: ${stringA}`)
...@@ -94,6 +124,12 @@ export const hexStringEquals = (stringA: string, stringB: string): boolean => { ...@@ -94,6 +124,12 @@ export const hexStringEquals = (stringA: string, stringB: string): boolean => {
return stringA.toLowerCase() === stringB.toLowerCase() return stringA.toLowerCase() === stringB.toLowerCase()
} }
/**
* Casts a number to a 32-byte, zero padded hex string.
*
* @param value Number to cast to a hex string.
* @return Number cast as a hex string.
*/
export const bytes32ify = (value: number | BigNumber): string => { export const bytes32ify = (value: number | BigNumber): string => {
return hexZeroPad(BigNumber.from(value).toHexString(), 32) return hexZeroPad(BigNumber.from(value).toHexString(), 32)
} }
...@@ -9,6 +9,9 @@ import { ...@@ -9,6 +9,9 @@ import {
fromHexString, fromHexString,
toHexString, toHexString,
padHexString, padHexString,
encodeHex,
hexStringEquals,
bytes32ify,
} from '../src' } from '../src'
describe('remove0x', () => { describe('remove0x', () => {
...@@ -52,13 +55,17 @@ describe('add0x', () => { ...@@ -52,13 +55,17 @@ describe('add0x', () => {
}) })
describe('toHexString', () => { describe('toHexString', () => {
it('should return undefined', () => { it('should throw an error when input is null', () => {
expect(add0x(undefined)).to.deep.equal(undefined) expect(() => {
toHexString(null)
}).to.throw(
'The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received null'
)
}) })
it('should return with a hex string', () => { it('should return with a hex string', () => {
const cases = [ const cases = [
{ input: 0, output: '0x00' }, { input: 0, output: '0x00' },
{ input: 48, output: '0x30' },
{ {
input: '0', input: '0',
output: '0x30', output: '0x30',
...@@ -122,3 +129,184 @@ describe('toRpcHexString', () => { ...@@ -122,3 +129,184 @@ describe('toRpcHexString', () => {
} }
}) })
}) })
describe('encodeHex', () => {
it('should throw an error when val is invalid', () => {
expect(() => {
encodeHex(null, 0)
}).to.throw('invalid BigNumber value')
expect(() => {
encodeHex(10.5, 0)
}).to.throw('fault="underflow", operation="BigNumber.from", value=10.5')
expect(() => {
encodeHex('10.5', 0)
}).to.throw('invalid BigNumber string')
})
it('should return a hex string of val with length len', () => {
const cases = [
{
input: {
val: 0,
len: 0,
},
output: '00',
},
{
input: {
val: 0,
len: 4,
},
output: '0000',
},
{
input: {
val: 1,
len: 0,
},
output: '01',
},
{
input: {
val: 1,
len: 10,
},
output: '0000000001',
},
{
input: {
val: 100,
len: 4,
},
output: '0064',
},
{
input: {
val: '100',
len: 0,
},
output: '64',
},
]
for (const test of cases) {
expect(encodeHex(test.input.val, test.input.len)).to.deep.equal(
test.output
)
}
})
})
describe('hexStringEquals', () => {
it('should throw an error when input is not a hex string', () => {
expect(() => {
hexStringEquals('', '')
}).to.throw('input is not a hex string: ')
expect(() => {
hexStringEquals('0xx', '0x1')
}).to.throw('input is not a hex string: 0xx')
expect(() => {
hexStringEquals('0x1', '2')
}).to.throw('input is not a hex string: 2')
expect(() => {
hexStringEquals('-0x1', '0x1')
}).to.throw('input is not a hex string: -0x1')
})
it('should return the hex strings equality', () => {
const cases = [
{
input: {
stringA: '0x',
stringB: '0x',
},
output: true,
},
{
input: {
stringA: '0x1',
stringB: '0x1',
},
output: true,
},
{
input: {
stringA: '0x064',
stringB: '0x064',
},
output: true,
},
{
input: {
stringA: '0x',
stringB: '0x0',
},
output: false,
},
{
input: {
stringA: '0x0',
stringB: '0x1',
},
output: false,
},
{
input: {
stringA: '0x64',
stringB: '0x064',
},
output: false,
},
]
for (const test of cases) {
expect(
hexStringEquals(test.input.stringA, test.input.stringB)
).to.deep.equal(test.output)
}
})
})
describe('bytes32ify', () => {
it('should throw an error when input is invalid', () => {
expect(() => {
bytes32ify(-1)
}).to.throw('invalid hex string')
})
it('should return a zero padded, 32 bytes hex string', () => {
const cases = [
{
input: 0,
output:
'0x0000000000000000000000000000000000000000000000000000000000000000',
},
{
input: BigNumber.from(0),
output:
'0x0000000000000000000000000000000000000000000000000000000000000000',
},
{
input: 2,
output:
'0x0000000000000000000000000000000000000000000000000000000000000002',
},
{
input: BigNumber.from(2),
output:
'0x0000000000000000000000000000000000000000000000000000000000000002',
},
{
input: 100,
output:
'0x0000000000000000000000000000000000000000000000000000000000000064',
},
]
for (const test of cases) {
expect(bytes32ify(test.input)).to.deep.equal(test.output)
}
})
})
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { predeploys } from '@eth-optimism/contracts'
import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
MessageLike,
NumberLike,
MessageDirection,
} from './interfaces'
import { omit } from './utils'
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,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.sendMessage(message, opts)
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,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
await this.populateTransaction.resendMessage(
message,
messageGasLimit,
opts
)
)
}
public async finalizeMessage(
message: MessageLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> {
throw new Error('Not implemented')
}
public async depositETH(
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l1Signer.sendTransaction(
await this.populateTransaction.depositETH(amount, opts)
)
}
public async withdrawETH(
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> {
return this.l2Signer.sendTransaction(
await this.populateTransaction.withdrawETH(amount, opts)
)
}
populateTransaction = {
sendMessage: async (
message: CrossChainMessageRequest,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.sendMessage(
message.target,
message.message,
opts?.l2GasLimit ||
(await this.provider.estimateL2MessageGasLimit(message)),
omit(opts?.overrides || {}, 'l2GasLimit')
)
} 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
omit(opts?.overrides || {}, 'l2GasLimit')
)
}
},
resendMessage: async (
message: MessageLike,
messageGasLimit: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const resolved = await this.provider.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot resend L2 to L1 message`)
}
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.replayMessage(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce,
resolved.gasLimit,
messageGasLimit,
opts?.overrides || {}
)
},
finalizeMessage: async (
message: MessageLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.provider.contracts.l1.L1StandardBridge.populateTransaction.depositETH(
opts?.l2GasLimit || 200000, // 200k gas is fine as a default
'0x', // No data
{
...omit(opts?.overrides || {}, 'l2GasLimit', 'value'),
value: amount,
}
)
},
withdrawETH: async (
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
return this.provider.contracts.l2.L2StandardBridge.populateTransaction.withdraw(
predeploys.OVM_ETH,
amount,
0, // No need to supply gas here
'0x', // No data,
opts?.overrides || {}
)
},
}
estimateGas = {
sendMessage: async (
message: CrossChainMessageRequest,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.sendMessage(message, opts)
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,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.resendMessage(
message,
messageGasLimit,
opts
)
return this.provider.l1Provider.estimateGas(tx)
},
finalizeMessage: async (
message: MessageLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
throw new Error('Not implemented')
},
depositETH: async (
amount: NumberLike,
opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.depositETH(amount, opts)
return this.provider.l1Provider.estimateGas(tx)
},
withdrawETH: async (
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
const tx = await this.populateTransaction.withdrawETH(amount, opts)
return this.provider.l2Provider.estimateGas(tx)
},
}
}
...@@ -12,11 +12,13 @@ import { ...@@ -12,11 +12,13 @@ import {
OEContracts, OEContracts,
OEContractsLike, OEContractsLike,
MessageLike, MessageLike,
MessageRequestLike,
TransactionLike, TransactionLike,
AddressLike, AddressLike,
NumberLike, NumberLike,
ProviderLike, ProviderLike,
CrossChainMessage, CrossChainMessage,
CrossChainMessageRequest,
MessageDirection, MessageDirection,
MessageStatus, MessageStatus,
TokenBridgeMessage, TokenBridgeMessage,
...@@ -24,6 +26,8 @@ import { ...@@ -24,6 +26,8 @@ import {
MessageReceiptStatus, MessageReceiptStatus,
CustomBridges, CustomBridges,
CustomBridgesLike, CustomBridgesLike,
StateRoot,
StateRootBatch,
} from './interfaces' } from './interfaces'
import { import {
toProvider, toProvider,
...@@ -50,6 +54,7 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -50,6 +54,7 @@ export class CrossChainProvider implements ICrossChainProvider {
* @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url. * @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain. * @param opts.l1ChainId Chain ID for the L1 chain.
* @param opts.contracts Optional contract address overrides. * @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/ */
constructor(opts: { constructor(opts: {
l1Provider: ProviderLike l1Provider: ProviderLike
...@@ -129,6 +134,7 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -129,6 +134,7 @@ export class CrossChainProvider implements ICrossChainProvider {
sender: parsed.args.sender, sender: parsed.args.sender,
message: parsed.args.message, message: parsed.args.message,
messageNonce: parsed.args.messageNonce, messageNonce: parsed.args.messageNonce,
gasLimit: parsed.args.gasLimit,
logIndex: log.logIndex, logIndex: log.logIndex,
blockNumber: log.blockNumber, blockNumber: log.blockNumber,
transactionHash: log.transactionHash, transactionHash: log.transactionHash,
...@@ -336,7 +342,44 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -336,7 +342,44 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> { 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( public async getMessageReceipt(
...@@ -421,12 +464,21 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -421,12 +464,21 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
public async estimateL2MessageGasLimit( public async estimateL2MessageGasLimit(
message: MessageLike, message: MessageRequestLike,
opts?: { opts?: {
bufferPercent?: number bufferPercent?: number
from?: string
} }
): Promise<BigNumber> { ): 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. // L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) { if (resolved.direction === MessageDirection.L2_TO_L1) {
...@@ -434,7 +486,7 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -434,7 +486,7 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
const estimate = await this.l2Provider.estimateGas({ const estimate = await this.l2Provider.estimateGas({
from: resolved.sender, from,
to: resolved.target, to: resolved.target,
data: resolved.message, data: resolved.message,
}) })
...@@ -450,9 +502,162 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -450,9 +502,162 @@ export class CrossChainProvider implements ICrossChainProvider {
throw new Error('Not implemented') 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 message: MessageLike
): Promise<number> { ): Promise<StateRoot | null> {
throw new Error('Not implemented') 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 './interfaces'
export * from './utils' export * from './utils'
export * from './cross-chain-provider' export * from './cross-chain-provider'
export * from './cross-chain-messenger'
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
TransactionResponse, TransactionResponse,
} from '@ethersproject/abstract-provider' } from '@ethersproject/abstract-provider'
import { NumberLike, L1ToL2Overrides } from './types' import { NumberLike } from './types'
import { ICrossChainMessenger } from './cross-chain-messenger' import { ICrossChainMessenger } from './cross-chain-messenger'
/** /**
...@@ -30,24 +30,32 @@ export interface ICrossChainERC20Pair { ...@@ -30,24 +30,32 @@ export interface ICrossChainERC20Pair {
* Deposits some tokens into the L2 chain. * Deposits some tokens into the L2 chain.
* *
* @param amount Amount of the token to deposit. * @param amount Amount of the token to deposit.
* @param overrides Optional transaction overrides. * @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction. * @returns Transaction response for the deposit transaction.
*/ */
deposit( deposit(
amount: NumberLike, amount: NumberLike,
overrides?: L1ToL2Overrides opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> ): Promise<TransactionResponse>
/** /**
* Withdraws some tokens back to the L1 chain. * Withdraws some tokens back to the L1 chain.
* *
* @param amount Amount of the token to withdraw. * @param amount Amount of the token to withdraw.
* @param overrides Optional transaction overrides. * @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction. * @returns Transaction response for the withdraw transaction.
*/ */
withdraw( withdraw(
amount: NumberLike, amount: NumberLike,
overrides?: Overrides opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> ): Promise<TransactionResponse>
/** /**
...@@ -59,24 +67,32 @@ export interface ICrossChainERC20Pair { ...@@ -59,24 +67,32 @@ export interface ICrossChainERC20Pair {
* Generates a transaction for depositing some tokens into the L2 chain. * Generates a transaction for depositing some tokens into the L2 chain.
* *
* @param amount Amount of the token to deposit. * @param amount Amount of the token to deposit.
* @param overrides Optional transaction overrides. * @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens. * @returns Transaction that can be signed and executed to deposit the tokens.
*/ */
deposit( deposit(
amount: NumberLike, amount: NumberLike,
overrides?: L1ToL2Overrides opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> ): Promise<TransactionResponse>
/** /**
* Generates a transaction for withdrawing some tokens back to the L1 chain. * Generates a transaction for withdrawing some tokens back to the L1 chain.
* *
* @param amount Amount of the token to withdraw. * @param amount Amount of the token to withdraw.
* @param overrides Optional transaction overrides. * @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens. * @returns Transaction that can be signed and executed to withdraw the tokens.
*/ */
withdraw( withdraw(
amount: NumberLike, amount: NumberLike,
overrides?: Overrides opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> ): Promise<TransactionRequest>
} }
...@@ -89,24 +105,32 @@ export interface ICrossChainERC20Pair { ...@@ -89,24 +105,32 @@ export interface ICrossChainERC20Pair {
* Estimates gas required to deposit some tokens into the L2 chain. * Estimates gas required to deposit some tokens into the L2 chain.
* *
* @param amount Amount of the token to deposit. * @param amount Amount of the token to deposit.
* @param overrides Optional transaction overrides. * @param opts Additional options.
* @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens. * @returns Transaction that can be signed and executed to deposit the tokens.
*/ */
deposit( deposit(
amount: NumberLike, amount: NumberLike,
overrides?: L1ToL2Overrides opts?: {
l2GasLimit?: NumberLike
overrides?: Overrides
}
): Promise<TransactionResponse> ): Promise<TransactionResponse>
/** /**
* Estimates gas required to withdraw some tokens back to the L1 chain. * Estimates gas required to withdraw some tokens back to the L1 chain.
* *
* @param amount Amount of the token to withdraw. * @param amount Amount of the token to withdraw.
* @param overrides Optional transaction overrides. * @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens. * @returns Transaction that can be signed and executed to withdraw the tokens.
*/ */
withdraw( withdraw(
amount: NumberLike, amount: NumberLike,
overrides?: Overrides opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> ): Promise<TransactionRequest>
} }
} }
import { BigNumber } from 'ethers' import { Event, BigNumber } from 'ethers'
import { Provider, BlockTag } from '@ethersproject/abstract-provider' import { Provider, BlockTag } from '@ethersproject/abstract-provider'
import { import {
MessageLike, MessageLike,
MessageRequestLike,
TransactionLike, TransactionLike,
AddressLike, AddressLike,
NumberLike, NumberLike,
...@@ -13,6 +14,8 @@ import { ...@@ -13,6 +14,8 @@ import {
OEContracts, OEContracts,
MessageReceipt, MessageReceipt,
CustomBridges, CustomBridges,
StateRoot,
StateRootBatch,
} from './types' } from './types'
/** /**
...@@ -203,12 +206,14 @@ export interface ICrossChainProvider { ...@@ -203,12 +206,14 @@ export interface ICrossChainProvider {
* @param message Message get a gas estimate for. * @param message Message get a gas estimate for.
* @param opts Options object. * @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20. * @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. * @returns Estimates L2 gas limit.
*/ */
estimateL2MessageGasLimit( estimateL2MessageGasLimit(
message: MessageLike, message: MessageRequestLike,
opts?: { opts?: {
bufferPercent?: number bufferPercent?: number
from?: string
} }
): Promise<BigNumber> ): Promise<BigNumber>
...@@ -224,13 +229,52 @@ export interface ICrossChainProvider { ...@@ -224,13 +229,52 @@ export interface ICrossChainProvider {
estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number> estimateMessageWaitTimeSeconds(message: MessageLike): Promise<number>
/** /**
* Returns the estimated amount of time before the message can be executed (in L1 blocks). * Queries the current challenge period in seconds from the StateCommitmentChain.
* 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.
* *
* @param message Message to estimate the time remaining for. * @returns Current challenge period in seconds.
* @returns Estimated amount of time remaining (in blocks) before the message can be executed. */
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>
} }
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
TransactionResponse, TransactionResponse,
} from '@ethersproject/abstract-provider' } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer' import { Signer } from '@ethersproject/abstract-signer'
import { Contract, BigNumber, Overrides } from 'ethers' import { Contract, BigNumber } from 'ethers'
/** /**
* L1 contract references. * L1 contract references.
...@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest { ...@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest {
direction: MessageDirection direction: MessageDirection
target: string target: string
message: string message: string
l2GasLimit: NumberLike
} }
/** /**
...@@ -162,6 +161,7 @@ export interface CoreCrossChainMessage { ...@@ -162,6 +161,7 @@ export interface CoreCrossChainMessage {
*/ */
export interface CrossChainMessage extends CoreCrossChainMessage { export interface CrossChainMessage extends CoreCrossChainMessage {
direction: MessageDirection direction: MessageDirection
gasLimit: number
logIndex: number logIndex: number
blockNumber: number blockNumber: number
transactionHash: string transactionHash: string
...@@ -212,20 +212,21 @@ export interface StateRootBatchHeader { ...@@ -212,20 +212,21 @@ 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 StateRootBatch { export interface StateRoot {
blockNumber: number
header: StateRootBatchHeader header: StateRootBatchHeader
stateRoots: string[] stateRoot: string
} }
/** /**
* Extended Ethers overrides object with an l2GasLimit field. * Information about a batch of state roots.
* Only meant to be used for L1 to L2 messages, since L2 to L1 messages don't have a specified gas
* limit field (gas used depends on the amount of gas provided).
*/ */
export type L1ToL2Overrides = Overrides & { export interface StateRootBatch {
l2GasLimit: NumberLike blockNumber: number
header: StateRootBatchHeader
stateRoots: string[]
} }
/** /**
...@@ -234,13 +235,22 @@ export type L1ToL2Overrides = Overrides & { ...@@ -234,13 +235,22 @@ export type L1ToL2Overrides = Overrides & {
export type TransactionLike = string | TransactionReceipt | TransactionResponse 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 = export type MessageLike =
| CrossChainMessage | CrossChainMessage
| TransactionLike | TransactionLike
| TokenBridgeMessage | TokenBridgeMessage
/**
* Stuff that can be coerced into a CrossChainMessageRequest.
*/
export type MessageRequestLike =
| CrossChainMessageRequest
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
/** /**
* Stuff that can be coerced into a provider. * Stuff that can be coerced into a provider.
*/ */
......
...@@ -3,6 +3,13 @@ pragma solidity ^0.8.9; ...@@ -3,6 +3,13 @@ pragma solidity ^0.8.9;
import { MockMessenger } from "./MockMessenger.sol"; import { MockMessenger } from "./MockMessenger.sol";
contract MockBridge { contract MockBridge {
event ETHDepositInitiated(
address indexed _from,
address indexed _to,
uint256 _amount,
bytes _data
);
event ERC20DepositInitiated( event ERC20DepositInitiated(
address indexed _l1Token, address indexed _l1Token,
address indexed _l2Token, address indexed _l2Token,
...@@ -110,4 +117,38 @@ contract MockBridge { ...@@ -110,4 +117,38 @@ contract MockBridge {
) public { ) public {
emit DepositFailed(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data); emit DepositFailed(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
} }
function depositETH(
uint32 _l2GasLimit,
bytes memory _data
)
public
payable
{
emit ETHDepositInitiated(
msg.sender,
msg.sender,
msg.value,
_data
);
}
function withdraw(
address _l2Token,
uint256 _amount,
uint32 _l1Gas,
bytes calldata _data
)
public
payable
{
emit WithdrawalInitiated(
address(0),
_l2Token,
msg.sender,
msg.sender,
_amount,
_data
);
}
} }
...@@ -7,13 +7,40 @@ contract MockMessenger is ICrossDomainMessenger { ...@@ -7,13 +7,40 @@ contract MockMessenger is ICrossDomainMessenger {
return address(0); return address(0);
} }
uint256 public nonce;
// Empty function to satisfy the interface. // Empty function to satisfy the interface.
function sendMessage( function sendMessage(
address _target, address _target,
bytes calldata _message, bytes calldata _message,
uint32 _gasLimit uint32 _gasLimit
) public { ) public {
return; emit SentMessage(
_target,
msg.sender,
_message,
nonce,
_gasLimit
);
nonce++;
}
function replayMessage(
address _target,
address _sender,
bytes calldata _message,
uint256 _queueIndex,
uint32 _oldGasLimit,
uint32 _newGasLimit
) public {
emit SentMessage(
_target,
_sender,
_message,
nonce,
_newGasLimit
);
nonce++;
} }
struct SentMessageEventParams { 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 { predeploys } from '@eth-optimism/contracts'
import { expect } from './setup'
import {
CrossChainProvider,
CrossChainMessenger,
MessageDirection,
} from '../src'
describe('CrossChainMessenger', () => { describe('CrossChainMessenger', () => {
let l1Signer: any
let l2Signer: any
before(async () => {
;[l1Signer, l2Signer] = await ethers.getSigners()
})
describe('sendMessage', () => { describe('sendMessage', () => {
describe('when no l2GasLimit is provided', () => { let l1Messenger: Contract
it('should send a message with an estimated l2GasLimit') 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', () => { describe('when the message is an L2 to L1 message', () => {
it('should send a message with the provided l2GasLimit') 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
)
})
}) })
}) })
describe('resendMessage', () => { describe('resendMessage', () => {
describe('when the message being resent exists', () => { let l1Messenger: Contract
it('should resend the message with the new gas limit') 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 being resent does not exist', () => { describe('when resending an L1 to L2 message', () => {
it('should throw an error') it('should resend the message with the new gas limit', async () => {
const message = {
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
const sent = await messenger.sendMessage(message, {
l2GasLimit: 1234,
})
await expect(messenger.resendMessage(sent, 10000))
.to.emit(l1Messenger, 'SentMessage')
.withArgs(
message.target,
await l1Signer.getAddress(),
message.message,
1, // nonce is now 1
10000
)
})
})
describe('when resending an L2 to L1 message', () => {
it('should throw an error', async () => {
const message = {
direction: MessageDirection.L2_TO_L1,
target: '0x' + '11'.repeat(20),
message: '0x' + '22'.repeat(32),
}
const sent = await messenger.sendMessage(message, {
l2GasLimit: 1234,
})
await expect(messenger.resendMessage(sent, 10000)).to.be.rejected
})
}) })
}) })
...@@ -40,4 +212,94 @@ describe('CrossChainMessenger', () => { ...@@ -40,4 +212,94 @@ describe('CrossChainMessenger', () => {
it('should throw an error') it('should throw an error')
}) })
}) })
describe('depositETH', () => {
let l1Messenger: Contract
let l1Bridge: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l1Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l1Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l1Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l1: {
L1CrossDomainMessenger: l1Messenger.address,
L1StandardBridge: l1Bridge.address,
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
it('should trigger the deposit ETH function with the given amount', async () => {
await expect(messenger.depositETH(100000))
.to.emit(l1Bridge, 'ETHDepositInitiated')
.withArgs(
await l1Signer.getAddress(),
await l1Signer.getAddress(),
100000,
'0x'
)
})
})
describe('withdrawETH', () => {
let l2Messenger: Contract
let l2Bridge: Contract
let provider: CrossChainProvider
let messenger: CrossChainMessenger
beforeEach(async () => {
l2Messenger = (await (
await ethers.getContractFactory('MockMessenger')
).deploy()) as any
l2Bridge = (await (
await ethers.getContractFactory('MockBridge')
).deploy(l2Messenger.address)) as any
provider = new CrossChainProvider({
l1Provider: ethers.provider,
l2Provider: ethers.provider,
l1ChainId: 31337,
contracts: {
l2: {
L2CrossDomainMessenger: l2Messenger.address,
L2StandardBridge: l2Bridge.address,
},
},
})
messenger = new CrossChainMessenger({
provider,
l1Signer,
l2Signer,
})
})
it('should trigger the deposit ETH function with the given amount', async () => {
await expect(messenger.withdrawETH(100000))
.to.emit(l2Bridge, 'WithdrawalInitiated')
.withArgs(
ethers.constants.AddressZero,
predeploys.OVM_ETH,
await l2Signer.getAddress(),
await l2Signer.getAddress(),
100000,
'0x'
)
})
})
}) })
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'
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