Commit 41cfbd16 authored by Georgios Konstantopoulos's avatar Georgios Konstantopoulos Committed by GitHub

chore: update to latest master part 2 (#53)

* feat(hardhat-ovm): allow ignoring libraries when compiling

https://github.com/ethereum-optimism/plugins/pull/39

* feat(batch-submitter): add logs

https://github.com/ethereum-optimism/batch-submitter/pull/75
https://github.com/ethereum-optimism/batch-submitter/pull/73

* chore: build contracts sequentially

this worsens our compilation times but ensures that the runs are not flaky
parent 0f8bac35
...@@ -69,6 +69,10 @@ export abstract class BatchSubmitter { ...@@ -69,6 +69,10 @@ export abstract class BatchSubmitter {
} }
await this._updateChainInfo() await this._updateChainInfo()
await this._checkBalance() await this._checkBalance()
this.log.info('Readying to submit next batch...', {
l2ChainId: this.l2ChainId,
batchSubmitterAddress: await this.signer.getAddress(),
})
if (this.syncing === true) { if (this.syncing === true) {
this.log.info( this.log.info(
...@@ -225,7 +229,7 @@ export abstract class BatchSubmitter { ...@@ -225,7 +229,7 @@ export abstract class BatchSubmitter {
this.log this.log
) )
this.log.debug('Transaction receipt:', { receipt }) this.log.info('Received transaction receipt', { receipt })
this.log.info(successMessage) this.log.info(successMessage)
return receipt return receipt
} }
......
...@@ -3,7 +3,7 @@ import { Promise as bPromise } from 'bluebird' ...@@ -3,7 +3,7 @@ import { Promise as bPromise } from 'bluebird'
import { Contract, Signer } from 'ethers' import { Contract, Signer } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider' import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { getContractFactory } from '@eth-optimism/contracts' import { getContractFactory } from '@eth-optimism/contracts'
import { Logger, Bytes32 } from '@eth-optimism/core-utils' import { Logger, Bytes32, remove0x } from '@eth-optimism/core-utils'
import { OptimismProvider } from '@eth-optimism/provider' import { OptimismProvider } from '@eth-optimism/provider'
/* Internal Imports */ /* Internal Imports */
...@@ -82,6 +82,10 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -82,6 +82,10 @@ export class StateBatchSubmitter extends BatchSubmitter {
sccAddress === this.chainContract.address && sccAddress === this.chainContract.address &&
ctcAddress === this.ctcContract.address ctcAddress === this.ctcContract.address
) { ) {
this.log.debug('Chain contract already initialized', {
sccAddress,
ctcAddress,
})
return return
} }
...@@ -105,12 +109,21 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -105,12 +109,21 @@ export class StateBatchSubmitter extends BatchSubmitter {
} }
public async _getBatchStartAndEnd(): Promise<Range> { public async _getBatchStartAndEnd(): Promise<Range> {
this.log.info('Getting batch start and end for state batch submitter...')
// TODO: Remove BLOCK_OFFSET by adding a tx to Geth's genesis // TODO: Remove BLOCK_OFFSET by adding a tx to Geth's genesis
const startBlock: number = const startBlock: number =
(await this.chainContract.getTotalElements()).toNumber() + BLOCK_OFFSET (await this.chainContract.getTotalElements()).toNumber() + BLOCK_OFFSET
this.log.info('Retrieved start block number from SCC', {
startBlock,
})
// We will submit state roots for txs which have been in the tx chain for a while. // We will submit state roots for txs which have been in the tx chain for a while.
const totalElements: number = const totalElements: number =
(await this.ctcContract.getTotalElements()).toNumber() + BLOCK_OFFSET (await this.ctcContract.getTotalElements()).toNumber() + BLOCK_OFFSET
this.log.info('Retrieved total elements from CTC', {
totalElements,
})
const endBlock: number = Math.min( const endBlock: number = Math.min(
startBlock + this.maxBatchSize, startBlock + this.maxBatchSize,
totalElements totalElements
...@@ -142,7 +155,13 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -142,7 +155,13 @@ export class StateBatchSubmitter extends BatchSubmitter {
'appendStateBatch', 'appendStateBatch',
[batch, startBlock] [batch, startBlock]
) )
if (!this._shouldSubmitBatch(tx.length / 2)) { const batchSizeInBytes = remove0x(tx).length / 2
this.log.debug('State batch generated', {
batchSizeInBytes,
tx,
})
if (!this._shouldSubmitBatch(batchSizeInBytes)) {
return return
} }
...@@ -156,6 +175,13 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -156,6 +175,13 @@ export class StateBatchSubmitter extends BatchSubmitter {
offsetStartsAtIndex, offsetStartsAtIndex,
{ nonce, gasPrice } { nonce, gasPrice }
) )
this.log.info('Submitted appendStateBatch transaction', {
nonce,
txHash: contractTx.hash,
contractAddr: this.chainContract.address,
from: contractTx.from,
data: contractTx.data,
})
return this.signer.provider.waitForTransaction( return this.signer.provider.waitForTransaction(
contractTx.hash, contractTx.hash,
this.numConfirmations this.numConfirmations
...@@ -180,7 +206,12 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -180,7 +206,12 @@ export class StateBatchSubmitter extends BatchSubmitter {
const block = (await this.l2Provider.getBlockWithTransactions( const block = (await this.l2Provider.getBlockWithTransactions(
startBlock + i startBlock + i
)) as L2Block )) as L2Block
if (block.transactions[0].from === this.fraudSubmissionAddress) { const blockTx = block.transactions[0]
if (blockTx.from === this.fraudSubmissionAddress) {
this.log.warn('Found transaction from fraud submission address', {
txHash: blockTx.hash,
fraudSubmissionAddress: this.fraudSubmissionAddress,
})
this.fraudSubmissionAddress = 'no fraud' this.fraudSubmissionAddress = 'no fraud'
return '0xbad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1' return '0xbad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1'
} }
...@@ -193,14 +224,20 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -193,14 +224,20 @@ export class StateBatchSubmitter extends BatchSubmitter {
'appendStateBatch', 'appendStateBatch',
[batch, startBlock] [batch, startBlock]
) )
while (tx.length > this.maxTxSize) { while (remove0x(tx).length / 2 > this.maxTxSize) {
batch.splice(Math.ceil((batch.length * 2) / 3)) // Delete 1/3rd of all of the batch elements batch.splice(Math.ceil((batch.length * 2) / 3)) // Delete 1/3rd of all of the batch elements
this.log.debug('Splicing batch...', {
batchSizeInBytes: tx.length / 2,
})
tx = this.chainContract.interface.encodeFunctionData('appendStateBatch', [ tx = this.chainContract.interface.encodeFunctionData('appendStateBatch', [
batch, batch,
startBlock, startBlock,
]) ])
} }
this.log.info('Generated state commitment batch', {
batch, // list of stateRoots
})
return batch return batch
} }
} }
...@@ -114,6 +114,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -114,6 +114,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
typeof this.chainContract !== 'undefined' && typeof this.chainContract !== 'undefined' &&
ctcAddress === this.chainContract.address ctcAddress === this.chainContract.address
) { ) {
this.log.debug('Chain contract already initialized', {
ctcAddress,
})
return return
} }
...@@ -134,6 +137,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -134,6 +137,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
public async _onSync(): Promise<TransactionReceipt> { public async _onSync(): Promise<TransactionReceipt> {
const pendingQueueElements = await this.chainContract.getNumPendingQueueElements() const pendingQueueElements = await this.chainContract.getNumPendingQueueElements()
this.log.debug('Got number of pending queue elements', {
pendingQueueElements,
})
if (pendingQueueElements !== 0) { if (pendingQueueElements !== 0) {
this.log.info( this.log.info(
...@@ -150,6 +156,13 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -150,6 +156,13 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
nonce, nonce,
gasPrice, gasPrice,
}) })
this.log.info('Submitted appendQueueBatch transaction', {
nonce,
txHash: tx.hash,
contractAddr: this.chainContract.address,
from: tx.from,
data: tx.data,
})
return this.signer.provider.waitForTransaction( return this.signer.provider.waitForTransaction(
tx.hash, tx.hash,
this.numConfirmations this.numConfirmations
...@@ -166,6 +179,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -166,6 +179,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
// TODO: Remove this function and use geth for lastL1BlockNumber! // TODO: Remove this function and use geth for lastL1BlockNumber!
private async _updateLastL1BlockNumber() { private async _updateLastL1BlockNumber() {
this.log.warn('Calling _updateLastL1BlockNumber...')
const pendingQueueElements = await this.chainContract.getNumPendingQueueElements() const pendingQueueElements = await this.chainContract.getNumPendingQueueElements()
if (pendingQueueElements !== 0) { if (pendingQueueElements !== 0) {
...@@ -187,17 +201,32 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -187,17 +201,32 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
} }
} }
} }
this.log.debug('Set lastL1BlockNumber', {
lastL1BlockNumber: this.lastL1BlockNumber,
})
} }
public async _getBatchStartAndEnd(): Promise<Range> { public async _getBatchStartAndEnd(): Promise<Range> {
this.log.info(
'Getting batch start and end for transaction batch submitter...'
)
// TODO: Remove BLOCK_OFFSET by adding a tx to Geth's genesis // TODO: Remove BLOCK_OFFSET by adding a tx to Geth's genesis
const startBlock = const startBlock =
(await this.chainContract.getTotalElements()).toNumber() + BLOCK_OFFSET (await this.chainContract.getTotalElements()).toNumber() + BLOCK_OFFSET
this.log.info('Retrieved start block number from CTC', {
startBlock,
})
const endBlock = const endBlock =
Math.min( Math.min(
startBlock + this.maxBatchSize, startBlock + this.maxBatchSize,
await this.l2Provider.getBlockNumber() await this.l2Provider.getBlockNumber()
) + 1 // +1 because the `endBlock` is *exclusive* ) + 1 // +1 because the `endBlock` is *exclusive*
this.log.info('Retrieved end block number from L2 sequencer', {
endBlock,
})
if (startBlock >= endBlock) { if (startBlock >= endBlock) {
if (startBlock > endBlock) { if (startBlock > endBlock) {
this.log this.log
...@@ -224,7 +253,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -224,7 +253,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
) )
if (gasPriceInGwei > this.gasThresholdInGwei) { if (gasPriceInGwei > this.gasThresholdInGwei) {
this.log.warn( this.log.warn(
'Gas price is higher than gras price threshold; aborting batch submission', 'Gas price is higher than gas price threshold; aborting batch submission',
{ {
gasPriceInGwei, gasPriceInGwei,
gasThresholdInGwei: this.gasThresholdInGwei, gasThresholdInGwei: this.gasThresholdInGwei,
...@@ -238,6 +267,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -238,6 +267,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
wasBatchTruncated, wasBatchTruncated,
] = await this._generateSequencerBatchParams(startBlock, endBlock) ] = await this._generateSequencerBatchParams(startBlock, endBlock)
const batchSizeInBytes = encodeAppendSequencerBatch(batchParams).length / 2 const batchSizeInBytes = encodeAppendSequencerBatch(batchParams).length / 2
this.log.debug('Sequencer batch generated', {
batchSizeInBytes,
})
// Only submit batch if one of the following is true: // Only submit batch if one of the following is true:
// 1. it was truncated // 1. it was truncated
...@@ -256,6 +288,13 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -256,6 +288,13 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
nonce, nonce,
gasPrice, gasPrice,
}) })
this.log.info('Submitted appendSequencerBatch transaction', {
nonce,
txHash: tx.hash,
contractAddr: this.chainContract.address,
from: tx.from,
data: tx.data,
})
return this.signer.provider.waitForTransaction( return this.signer.provider.waitForTransaction(
tx.hash, tx.hash,
this.numConfirmations this.numConfirmations
...@@ -299,6 +338,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -299,6 +338,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
let wasBatchTruncated = false let wasBatchTruncated = false
let encoded = encodeAppendSequencerBatch(sequencerBatchParams) let encoded = encodeAppendSequencerBatch(sequencerBatchParams)
while (encoded.length / 2 > this.maxTxSize) { while (encoded.length / 2 > this.maxTxSize) {
this.log.debug('Splicing batch...', {
batchSizeInBytes: encoded.length / 2,
})
batch.splice(Math.ceil((batch.length * 2) / 3)) // Delete 1/3rd of all of the batch elements batch.splice(Math.ceil((batch.length * 2) / 3)) // Delete 1/3rd of all of the batch elements
sequencerBatchParams = await this._getSequencerBatchParams( sequencerBatchParams = await this._getSequencerBatchParams(
startBlock, startBlock,
...@@ -310,6 +352,12 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -310,6 +352,12 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
// In this case, we want to submit regardless of the batch's size. // In this case, we want to submit regardless of the batch's size.
wasBatchTruncated = true wasBatchTruncated = true
} }
this.log.info('Generated sequencer batch params', {
contexts: sequencerBatchParams.contexts,
transactions: sequencerBatchParams.transactions,
wasBatchTruncated,
})
return [sequencerBatchParams, wasBatchTruncated] return [sequencerBatchParams, wasBatchTruncated]
} }
...@@ -421,6 +469,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -421,6 +469,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
// TODO: Remove this super complex logic and rely on Geth to actually supply correct block data. // TODO: Remove this super complex logic and rely on Geth to actually supply correct block data.
const fixMonotonicity = async (b: Batch): Promise<Batch> => { const fixMonotonicity = async (b: Batch): Promise<Batch> => {
this.log.debug('Fixing monotonicity...')
// The earliest allowed timestamp/blockNumber is the last timestamp submitted on chain. // The earliest allowed timestamp/blockNumber is the last timestamp submitted on chain.
const { const {
lastTimestamp, lastTimestamp,
...@@ -428,6 +477,10 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -428,6 +477,10 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
} = await this._getLastTimestampAndBlockNumber() } = await this._getLastTimestampAndBlockNumber()
let earliestTimestamp = lastTimestamp let earliestTimestamp = lastTimestamp
let earliestBlockNumber = lastBlockNumber let earliestBlockNumber = lastBlockNumber
this.log.debug('Determined earliest timestamp and blockNumber', {
earliestTimestamp,
earliestBlockNumber,
})
// The latest allowed timestamp/blockNumber is the next queue element! // The latest allowed timestamp/blockNumber is the next queue element!
let nextQueueIndex = await this.chainContract.getNextQueueIndex() let nextQueueIndex = await this.chainContract.getNextQueueIndex()
...@@ -457,6 +510,10 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -457,6 +510,10 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
} }
// Actually update the latest timestamp and block number // Actually update the latest timestamp and block number
await updateLatestTimestampAndBlockNumber() await updateLatestTimestampAndBlockNumber()
this.log.debug('Determined latest timestamp and blockNumber', {
latestTimestamp,
latestBlockNumber,
})
// Now go through our batch and fix the timestamps and block numbers // Now go through our batch and fix the timestamps and block numbers
// to automatically enforce monotonicity. // to automatically enforce monotonicity.
...@@ -573,7 +630,13 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -573,7 +630,13 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
blockNumber > queueElement.blockNumber blockNumber > queueElement.blockNumber
) { ) {
this.log.warn( this.log.warn(
'Double deposit detected!!! Fixing by skipping the deposit & replacing with a dummy tx.' 'Double deposit detected. Fixing by skipping the deposit & replacing with a dummy tx.',
{
timestamp,
blockNumber,
queueElementTimestamp: queueElement.timestamp,
queueElementBlockNumber: queueElement.blockNumber,
}
) )
// This implies that we've double played a deposit. // This implies that we've double played a deposit.
// We can correct this by instead submitting a dummy sequencer tx // We can correct this by instead submitting a dummy sequencer tx
...@@ -712,6 +775,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -712,6 +775,9 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
private async _getL2BatchElement(blockNumber: number): Promise<BatchElement> { private async _getL2BatchElement(blockNumber: number): Promise<BatchElement> {
const block = await this._getBlock(blockNumber) const block = await this._getBlock(blockNumber)
this.log.debug('Fetched L2 block', {
block,
})
const txType = block.transactions[0].txType const txType = block.transactions[0].txType
if (this._isSequencerTx(block)) { if (this._isSequencerTx(block)) {
...@@ -744,6 +810,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -744,6 +810,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
if (!block.transactions[0].l1BlockNumber) { if (!block.transactions[0].l1BlockNumber) {
block.transactions[0].l1BlockNumber = this.lastL1BlockNumber block.transactions[0].l1BlockNumber = this.lastL1BlockNumber
} }
return block return block
} }
......
...@@ -201,9 +201,12 @@ export const run = async () => { ...@@ -201,9 +201,12 @@ export const run = async () => {
value: 0, value: 0,
nonce: i, nonce: i,
}) })
log.info('Submitting empty transaction', { log.info('Submitted empty transaction', {
nonce: i, nonce: i,
txHash: response.hash, txHash: response.hash,
to: response.to,
from: response.from,
data: response.data,
}) })
await sequencerSigner.provider.waitForTransaction( await sequencerSigner.provider.waitForTransaction(
response.hash, response.hash,
......
export * from './batch-submitter' export * from './batch-submitter'
export * from './utils'
export * from './transaction-chain-contract' export * from './transaction-chain-contract'
export * from './types' export * from './types'
...@@ -5,11 +5,11 @@ import { ...@@ -5,11 +5,11 @@ import {
TransactionRequest, TransactionRequest,
} from '@ethersproject/abstract-provider' } from '@ethersproject/abstract-provider'
import { keccak256 } from 'ethers/lib/utils' import { keccak256 } from 'ethers/lib/utils'
import { remove0x, encodeHex } from './utils'
import { import {
AppendSequencerBatchParams, AppendSequencerBatchParams,
BatchContext, BatchContext,
encodeAppendSequencerBatch, encodeAppendSequencerBatch,
encodeHex,
} from '@eth-optimism/core-utils' } from '@eth-optimism/core-utils'
export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams } export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
......
yarn run build:typescript & yarn run build:contracts & yarn run build:contracts:ovm yarn run build:typescript & yarn run build:contracts
yarn run build:contracts:ovm
yarn run build:copy:artifacts & yarn run build:copy:artifacts:ovm & yarn run build:copy:contracts yarn run build:copy:artifacts & yarn run build:copy:artifacts:ovm & yarn run build:copy:contracts
...@@ -75,7 +75,19 @@ const getOvmSolcPath = async (version: string): Promise<string> => { ...@@ -75,7 +75,19 @@ const getOvmSolcPath = async (version: string): Promise<string> => {
subtask( subtask(
TASK_COMPILE_SOLIDITY_RUN_SOLC, TASK_COMPILE_SOLIDITY_RUN_SOLC,
async (args: { input: any; solcPath: string }, hre, runSuper) => { async (args: { input: any; solcPath: string }, hre, runSuper) => {
const ignoreRxList = hre.network.config.ignoreRxList || [];
const ignore = (filename: string) => ignoreRxList.reduce((ignored: boolean, rx: string | RegExp) => ignored || new RegExp(rx).test(filename), false);
if (hre.network.ovm !== true) { if (hre.network.ovm !== true) {
// Separate the EVM and OVM inputs.
for (const file of Object.keys(args.input.sources)) {
// Ignore any contract that has this tag or in ignore list
if (args.input.sources[file].content.includes('// @unsupported: evm') || ignore(file)) {
delete args.input.sources[file];
}
else {
//console.log(file + ' included');
}
}
return runSuper(args) return runSuper(args)
} }
...@@ -101,8 +113,8 @@ subtask( ...@@ -101,8 +113,8 @@ subtask(
// Separate the EVM and OVM inputs. // Separate the EVM and OVM inputs.
for (const file of Object.keys(args.input.sources)) { for (const file of Object.keys(args.input.sources)) {
// Ignore any contract that has this tag. // Ignore any contract that has this tag or in ignore list
if (!args.input.sources[file].content.includes('// @unsupported: ovm')) { if (!args.input.sources[file].content.includes('// @unsupported: ovm') && !ignore(file)) {
ovmInput.sources[file] = args.input.sources[file] ovmInput.sources[file] = args.input.sources[file]
} }
} }
...@@ -141,8 +153,8 @@ extendEnvironment((hre) => { ...@@ -141,8 +153,8 @@ extendEnvironment((hre) => {
} }
// Forcibly update the artifacts object. // Forcibly update the artifacts object.
hre.config.paths.artifacts = artifactsPath hre.config.paths.artifacts = artifactsPath;
hre.config.paths.cache = cachePath hre.config.paths.cache = cachePath;
;(hre as any).artifacts = new Artifacts(artifactsPath) (hre as any).artifacts = new Artifacts(artifactsPath);
} }
}) })
...@@ -14,24 +14,31 @@ declare module 'hardhat/types/config' { ...@@ -14,24 +14,31 @@ declare module 'hardhat/types/config' {
} }
interface HardhatNetworkUserConfig { interface HardhatNetworkUserConfig {
ovm?: boolean ovm?: boolean;
ignoreRxList?: string[];
} }
interface HttpNetworkUserConfig { interface HttpNetworkUserConfig {
ovm?: boolean ovm?: boolean;
ignoreRxList?: string[];
} }
interface HardhatNetworkConfig { interface HardhatNetworkConfig {
ovm: boolean ovm: boolean;
ignoreRxList: string[];
} }
interface HttpNetworkConfig { interface HttpNetworkConfig {
ovm: boolean ovm: boolean;
ignoreRxList: string[];
} }
} }
declare module 'hardhat/types/runtime' { declare module 'hardhat/types/runtime' {
interface Network { interface Network {
ovm: boolean ovm: boolean;
ignoreRxList: string[];
} }
} }
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