diff --git a/local-network/deploy/src/waves-utils.ts b/local-network/deploy/src/waves-utils.ts index a95c9478..d31074f4 100644 --- a/local-network/deploy/src/waves-utils.ts +++ b/local-network/deploy/src/waves-utils.ts @@ -8,7 +8,7 @@ export type LibraryWavesApi = ReturnType; export type ExtendedWavesApi = LibraryWavesApi & { base: string }; export type WavesSignedTransaction = SignedTransaction & { id: string }; -export interface EcBlockContractInfo { +export interface BlockContractInfo { chainHeight: number, epochNumber: number } @@ -126,7 +126,7 @@ export async function signAndBroadcast(wavesApi: ExtendedWavesApi, name: string, if (options.wait) await waitForTxn(wavesApi, id).then(x => logger.debug(`Sent %O result: %O`, unsignedTxJson, x)); } -function parseBlockMeta(response: object): EcBlockContractInfo { +function parseBlockMeta(response: object): BlockContractInfo { // @ts-ignore: Property 'value' does not exist on type 'object'. const rawMeta = response.result.value; return { @@ -135,7 +135,7 @@ function parseBlockMeta(response: object): EcBlockContractInfo { }; } -export async function waitForEcBlock(wavesApi: LibraryWavesApi, chainContractAddress: string, blockHash: string): Promise { +export async function waitForBlock(wavesApi: LibraryWavesApi, chainContractAddress: string, blockHash: string): Promise { const getBlockData = async () => { try { return parseBlockMeta(await wavesApi.utils.fetchEvaluate(chainContractAddress, `blockMeta("${blockHash.slice(2)}")`)); @@ -168,7 +168,7 @@ export function prepareE2CWithdrawTxnJson( }; } -export async function chainContractCurrFinalizedBlock(wavesApi: LibraryWavesApi, chainContractAddress: string): Promise { +export async function chainContractCurrFinalizedBlock(wavesApi: LibraryWavesApi, chainContractAddress: string): Promise { // @ts-ignore: Property 'value' does not exist on type 'object'. return parseBlockMeta(await wavesApi.utils.fetchEvaluate(chainContractAddress, `blockMeta(getStringValue("finalizedBlock"))`)); } diff --git a/local-network/deploy/test/miner_big-join.ts b/local-network/deploy/test/miner_big-join.ts deleted file mode 100644 index 6e1841c8..00000000 --- a/local-network/deploy/test/miner_big-join.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as logger from '../src/logger'; -import { setup } from '../setup'; - -const { waves } = await setup(false); - -logger.info('Join a miner with the balance of 50% + 1'); - -const unsignedTxnJson = { - sender: "3FSrRN8X7cDsLyYTScS8Yf8KSwZgJBwf1jU", - feeAssetId: null, - fee: 2400000, - version: 2, - type: 12, - data: [ - { - key: "%s__3FSsLw2bximwiBGP1KNTjbTHJVgYiNBojrS", - type: "string", - value: "%d%d%d%d__9__5500001__40__5500001" - } - ] -}; - -await waves.utils.signAndBroadcast(waves.wavesApi2, 'join big', unsignedTxnJson, { wait: true }); diff --git a/local-network/deploy/test/transfer-e2c.ts b/local-network/deploy/test/transfer-e2c.ts index 11edb11e..a6b498a7 100644 --- a/local-network/deploy/test/transfer-e2c.ts +++ b/local-network/deploy/test/transfer-e2c.ts @@ -57,7 +57,7 @@ if (rawLogsInBlock.length == 0) throw new Error(`Can't find logs in ${blockHash} const logsInBlock = rawLogsInBlock as Exclude[]; logger.info(`Waiting EL block ${blockHash} confirmation on CL`); -const withdrawBlockMeta = await waves.utils.waitForEcBlock(waves.wavesApi1, waves.chainContractAddress, blockHash); +const withdrawBlockMeta = await waves.utils.waitForBlock(waves.wavesApi1, waves.chainContractAddress, blockHash); logger.info(`Withdraw block meta: %O`, withdrawBlockMeta); let rawData: string[] = []; diff --git a/src/main/scala/units/BlockHash.scala b/src/main/scala/units/BlockHash.scala index 2a8c92fe..73210821 100644 --- a/src/main/scala/units/BlockHash.scala +++ b/src/main/scala/units/BlockHash.scala @@ -6,16 +6,20 @@ import play.api.libs.json.{Format, Reads, Writes} import supertagged.TaggedType object BlockHash extends TaggedType[String] { + + val BytesSize: Int = 32 + val HexSize: Int = 66 + def apply(hex: String): BlockHash = { require(hex.startsWith("0x"), "Expected hash to start with 0x") - require(hex.length == 66, s"Expected hash size of 66, got: ${hex.length}. Hex: $hex") // "0x" + 32 bytes + require(hex.length == HexSize, s"Expected hash size of $HexSize, got: ${hex.length}. Hex: $hex") // "0x" + 32 bytes BlockHash @@ hex } def apply(xs: ByteStr): BlockHash = BlockHash @@ HexBytesConverter.toHex(xs) def apply(xs: Array[Byte]): BlockHash = { - require(xs.length == 32, "Block hash size must be 32 bytes") + require(xs.length == BytesSize, s"Block hash size must be $BytesSize bytes") BlockHash @@ HexBytesConverter.toHex(xs) } diff --git a/src/main/scala/units/ClientError.scala b/src/main/scala/units/ClientError.scala index 313c0477..706db9f6 100644 --- a/src/main/scala/units/ClientError.scala +++ b/src/main/scala/units/ClientError.scala @@ -1,3 +1,4 @@ package units +// TODO: maybe remove? case class ClientError(message: String) diff --git a/src/main/scala/units/ConsensusClient.scala b/src/main/scala/units/ConsensusClient.scala index 70be3ad9..f93e3931 100644 --- a/src/main/scala/units/ConsensusClient.scala +++ b/src/main/scala/units/ConsensusClient.scala @@ -16,6 +16,7 @@ import net.ceedubs.ficus.Ficus.* import org.slf4j.LoggerFactory import sttp.client3.HttpClientSyncBackend import units.client.JwtAuthenticationBackend +import units.client.contract.{ChainContractClient, ChainContractStateClient} import units.client.engine.{EngineApiClient, HttpEngineApiClient, LoggedEngineApiClient} import units.network.* @@ -27,8 +28,8 @@ class ConsensusClient( config: ClientConfig, context: ExtensionContext, engineApiClient: EngineApiClient, - blockObserver: BlocksObserver, - allChannels: DefaultChannelGroup, + chainContractClient: ChainContractClient, + payloadObserver: PayloadObserver, globalScheduler: Scheduler, eluScheduler: Scheduler, ownedResources: AutoCloseable @@ -40,8 +41,8 @@ class ConsensusClient( deps.config, context, deps.engineApiClient, - deps.blockObserver, - deps.allChannels, + deps.chainContractClient, + deps.payloadObserver, deps.globalScheduler, deps.eluScheduler, deps @@ -52,25 +53,25 @@ class ConsensusClient( private[units] val elu = new ELUpdater( engineApiClient, + chainContractClient, context.blockchain, context.utx, - allChannels, + payloadObserver, config, context.time, context.wallet, - blockObserver.loadBlock, context.broadcastTransaction, eluScheduler, globalScheduler ) - private val blocksStreamCancelable: CancelableFuture[Unit] = - blockObserver.getBlockStream.foreach { case (ch, block) => elu.executionBlockReceived(block, ch) }(globalScheduler) + private val payloadsStreamCancelable: CancelableFuture[Unit] = + payloadObserver.getPayloadStream.foreach(elu.executionPayloadReceived)(globalScheduler) override def start(): Unit = {} def shutdown(): Future[Unit] = Future { - blocksStreamCancelable.cancel() + payloadsStreamCancelable.cancel() ownedResources.close() }(globalScheduler) @@ -101,9 +102,9 @@ class ConsensusClientDependencies(context: ExtensionContext) extends AutoCloseab val config: ClientConfig = context.settings.config.as[ClientConfig]("waves.l2") - private val blockObserverScheduler = Schedulers.singleThread("block-observer-l2", reporter = { e => log.warn("Error in BlockObserver", e) }) - val globalScheduler: Scheduler = monix.execution.Scheduler.global - val eluScheduler: SchedulerService = Scheduler.singleThread("el-updater", reporter = { e => log.warn("Exception in ELUpdater", e) }) + private val payloadObserverScheduler = Schedulers.singleThread("payload-observer-l2", reporter = { e => log.warn("Error in PayloadObserver", e) }) + val globalScheduler: Scheduler = monix.execution.Scheduler.global + val eluScheduler: SchedulerService = Scheduler.singleThread("el-updater", reporter = { e => log.warn("Exception in ELUpdater", e) }) private val httpClientBackend = HttpClientSyncBackend() private val maybeAuthenticatedBackend = config.jwtSecretFile match { @@ -118,6 +119,9 @@ class ConsensusClientDependencies(context: ExtensionContext) extends AutoCloseab val engineApiClient = new LoggedEngineApiClient(new HttpEngineApiClient(config, maybeAuthenticatedBackend)) + private val contractAddress = config.chainContractAddress + val chainContractClient = new ChainContractStateClient(contractAddress, context.blockchain) + val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE) val peerDatabase = new PeerDatabaseImpl(config.network) val messageObserver = new MessageObserver() @@ -130,7 +134,10 @@ class ConsensusClientDependencies(context: ExtensionContext) extends AutoCloseab new ConcurrentHashMap[Channel, PeerInfo] ) - val blockObserver = new BlocksObserverImpl(allChannels, messageObserver.blocks, config.blockSyncRequestTimeout)(blockObserverScheduler) + val payloadObserver = + new PayloadObserverImpl(allChannels, messageObserver.payloads, chainContractClient.getMinersPks, config.blockSyncRequestTimeout)( + payloadObserverScheduler + ) override def close(): Unit = { log.info("Closing HTTP/Engine API") @@ -144,7 +151,7 @@ class ConsensusClientDependencies(context: ExtensionContext) extends AutoCloseab messageObserver.shutdown() log.info("Closing schedulers") - blockObserverScheduler.shutdown() + payloadObserverScheduler.shutdown() eluScheduler.shutdown() } } diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index 714adf57..1b08afd2 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -9,7 +9,6 @@ import com.wavesplatform.common.state.ByteStr import com.wavesplatform.crypto import com.wavesplatform.lang.ValidationError import com.wavesplatform.lang.v1.compiler.Terms.FUNCTION_CALL -import com.wavesplatform.network.ChannelGroupExt import com.wavesplatform.state.Blockchain import com.wavesplatform.state.diffs.FeeValidation.{FeeConstants, FeeUnit, ScriptExtraFee} import com.wavesplatform.state.diffs.TransactionDiffer.TransactionValidationError @@ -20,21 +19,18 @@ import com.wavesplatform.transaction.{Asset, Proofs, Transaction, TransactionSig import com.wavesplatform.utils.{EthEncoding, Time, UnsupportedFeature, forceStopApplication} import com.wavesplatform.utx.UtxPool import com.wavesplatform.wallet.Wallet -import io.netty.channel.Channel -import io.netty.channel.group.DefaultChannelGroup import monix.execution.cancelables.SerialCancelable import monix.execution.{CancelableFuture, Scheduler} -import play.api.libs.json.* import units.ELUpdater.State.* import units.ELUpdater.State.ChainStatus.{FollowingChain, Mining, WaitForNewChain} -import units.client.L2BlockLike +import units.client.CommonBlockData import units.client.contract.* import units.client.engine.EngineApiClient import units.client.engine.EngineApiClient.PayloadId import units.client.engine.model.* import units.client.engine.model.Withdrawal.WithdrawalIndex -import units.eth.{EmptyL2Block, EthAddress, EthereumConstants} -import units.network.BlocksObserverImpl.BlockWithChannel +import units.eth.{EmptyPayload, EthAddress, EthereumConstants} +import units.network.PayloadObserver import units.util.HexBytesConverter import units.util.HexBytesConverter.toHexNoPrefix @@ -44,37 +40,36 @@ import scala.util.* class ELUpdater( engineApiClient: EngineApiClient, + chainContractClient: ChainContractClient, blockchain: Blockchain, utx: UtxPool, - allChannels: DefaultChannelGroup, + payloadObserver: PayloadObserver, config: ClientConfig, time: Time, wallet: Wallet, - requestBlockFromPeers: BlockHash => CancelableFuture[BlockWithChannel], broadcastTx: Transaction => TracedResult[ValidationError, Boolean], scheduler: Scheduler, globalScheduler: Scheduler ) extends StrictLogging { import ELUpdater.* - private val handleNextUpdate = SerialCancelable() - private val contractAddress = config.chainContractAddress - private val chainContractClient = new ChainContractStateClient(contractAddress, blockchain) + private val handleNextUpdate = SerialCancelable() private[units] var state: State = Starting def consensusLayerChanged(): Unit = handleNextUpdate := scheduler.scheduleOnce(ClChangedProcessingDelay)(handleConsensusLayerChanged()) - def executionBlockReceived(block: NetworkL2Block, ch: Channel): Unit = scheduler.execute { () => - logger.debug(s"New block ${block.hash}->${block.parentHash} (timestamp=${block.timestamp}, height=${block.height}) appeared") + def executionPayloadReceived(epi: ExecutionPayloadInfo): Unit = scheduler.execute { () => + val payload = epi.payload + logger.debug(s"New payload for block ${payload.hash}->${payload.parentHash} (timestamp=${payload.timestamp}, height=${payload.height}) appeared") val now = time.correctedTime() / 1000 - if (block.timestamp - now <= MaxTimeDrift) { + if (payload.timestamp - now <= MaxTimeDrift) { state match { - case WaitingForSyncHead(target, _) if block.hash == target.hash => + case WaitingForSyncHead(target, _) if payload.hash == target.hash => val syncStarted = for { - _ <- engineApiClient.applyNewPayload(block.payload) + _ <- engineApiClient.applyNewPayload(epi.payloadJson) fcuStatus <- confirmBlock(target, target) } yield fcuStatus @@ -87,27 +82,27 @@ class ELUpdater( logger.debug(s"Waiting for sync completion: $fcuStatus") waitForSyncCompletion(target) } - case w @ Working(_, lastEcBlock, _, _, _, FollowingChain(nodeChainInfo, _), _, returnToMainChainInfo) - if block.parentHash == lastEcBlock.hash => - validateAndApply(block, ch, w, lastEcBlock, nodeChainInfo, returnToMainChainInfo) + case w @ Working(_, lastPayload, _, _, _, FollowingChain(nodeChainInfo, _), _, returnToMainChainInfo) + if payload.parentHash == lastPayload.hash => + validateAndApply(epi, w, lastPayload, nodeChainInfo, returnToMainChainInfo) case w: Working[ChainStatus] => w.returnToMainChainInfo match { - case Some(rInfo) if rInfo.missedBlock.hash == block.hash => + case Some(rInfo) if rInfo.missedBlock.hash == payload.hash => chainContractClient.getChainInfo(rInfo.chainId) match { case Some(chainInfo) if chainInfo.isMain => - validateAndApplyMissedBlock(block, ch, w, rInfo.missedBlock, rInfo.missedBlockParent, chainInfo) + validateAndApplyMissed(epi, w, rInfo.missedBlock, rInfo.missedBlockParentPayload, chainInfo) case Some(_) => - logger.debug(s"Chain ${rInfo.chainId} is not main anymore, ignoring ${block.hash}") + logger.debug(s"Chain ${rInfo.chainId} is not main anymore, ignoring ${payload.hash}") case _ => - logger.error(s"Failed to get chain ${rInfo.chainId} info, ignoring ${block.hash}") + logger.error(s"Failed to get chain ${rInfo.chainId} info, ignoring ${payload.hash}") } - case _ => logger.debug(s"Expecting ${w.returnToMainChainInfo.fold("no block")(_.toString)}, ignoring unexpected ${block.hash}") + case _ => logger.debug(s"Expecting ${w.returnToMainChainInfo.fold("no block payload")(_.toString)}, ignoring unexpected ${payload.hash}") } case other => - logger.debug(s"$other: ignoring ${block.hash}") + logger.debug(s"$other: ignoring ${payload.hash}") } } else { - logger.debug(s"Block ${block.hash} is from future: timestamp=${block.timestamp}, now=$now, Δ${block.timestamp - now}s") + logger.debug(s"Payload for block ${payload.hash} is from future: timestamp=${payload.timestamp}, now=$now, Δ${payload.timestamp - now}s") } } @@ -153,7 +148,7 @@ class ELUpdater( // Removing here, because we have these transactions in PP after the onProcessBlock trigger utx.getPriorityPool.foreach { pp => val staleTxs = pp.priorityTransactions.filter { - case tx: InvokeScriptTransaction => tx.dApp == contractAddress + case tx: InvokeScriptTransaction => tx.dApp == config.chainContractAddress case _ => false } @@ -164,13 +159,13 @@ class ELUpdater( } } - private def callContract(fc: FUNCTION_CALL, blockData: EcBlock, invoker: KeyPair): JobResult[Unit] = { + private def callContract(fc: FUNCTION_CALL, payload: ExecutionPayload, invoker: KeyPair): JobResult[Unit] = { val extraFee = if (blockchain.hasPaidVerifier(invoker.toAddress)) ScriptExtraFee else 0 val tx = InvokeScriptTransaction( TxVersion.V2, invoker.publicKey, - contractAddress, + config.chainContractAddress, Some(fc), Seq.empty, TxPositiveAmount.unsafeFrom(FeeConstants(TransactionType.InvokeScript) * FeeUnit + extraFee), @@ -179,7 +174,9 @@ class ELUpdater( Proofs.empty, blockchain.settings.addressSchemeCharacter.toByte ).signWith(invoker.privateKey) - logger.info(s"Invoking $contractAddress '${fc.function.funcName}' for block ${blockData.hash}->${blockData.parentHash}, txId=${tx.id()}") + logger.info( + s"Invoking ${config.chainContractAddress} '${fc.function.funcName}' for block ${payload.hash}->${payload.parentHash}, txId=${tx.id()}" + ) cleanPriorityPool() broadcastTx(tx).resultE match { @@ -226,27 +223,20 @@ class ELUpdater( ) case _ => (for { - payload <- engineApiClient.getPayload(payloadId) + payloadJson <- engineApiClient.getPayload(payloadId) _ = logger.info(s"Forged payload $payloadId") - latestValidHashOpt <- engineApiClient.applyNewPayload(payload) + latestValidHashOpt <- engineApiClient.applyNewPayload(payloadJson) latestValidHash <- Either.fromOption(latestValidHashOpt, ClientError("Latest valid hash not defined")) _ = logger.info(s"Applied payload $payloadId, block hash is $latestValidHash, timestamp = $timestamp") - newBlock <- NetworkL2Block.signed(payload, m.keyPair.privateKey) - _ = logger.debug(s"Broadcasting block ${newBlock.hash}") - _ <- Try(allChannels.broadcast(newBlock)).toEither.leftMap(err => - ClientError(s"Failed to broadcast block ${newBlock.hash}: ${err.toString}") - ) - ecBlock = newBlock.toEcBlock - transfersRootHash <- getE2CTransfersRootHash(ecBlock.hash, chainContractOptions.elBridgeAddress) - funcCall <- contractFunction.toFunctionCall(ecBlock.hash, transfersRootHash, m.lastC2ETransferIndex) - _ <- callContract( - funcCall, - ecBlock, - m.keyPair - ) - } yield ecBlock).fold( + newPm <- payloadObserver.broadcastSigned(payloadJson, m.keyPair.privateKey).leftMap(ClientError.apply) + payloadInfo <- newPm.payloadInfo.leftMap(ClientError.apply) + payload = payloadInfo.payload + transfersRootHash <- getE2CTransfersRootHash(payload.hash, chainContractOptions.elBridgeAddress) + funcCall <- contractFunction.toFunctionCall(payload.hash, transfersRootHash, m.lastC2ETransferIndex) + _ <- callContract(funcCall, payload, m.keyPair) + } yield payload).fold( err => logger.error(s"Failed to forge block for payloadId $payloadId at epoch ${epochInfo.number}: ${err.message}"), - newEcBlock => scheduler.execute { () => tryToForgeNextBlock(epochInfo.number, newEcBlock, chainContractOptions) } + newPayload => scheduler.execute { () => tryToForgeNextBlock(epochInfo.number, newPayload, chainContractOptions) } ) } case Working(_, _, _, _, _, _: Mining | _: FollowingChain, _, _) => @@ -257,30 +247,30 @@ class ELUpdater( } } - private def rollbackTo(prevState: Working[ChainStatus], target: L2BlockLike, finalizedBlock: ContractBlock): JobResult[Working[ChainStatus]] = { + private def rollbackTo(prevState: Working[ChainStatus], target: CommonBlockData, finalizedBlock: ContractBlock): JobResult[Working[ChainStatus]] = { val targetHash = target.hash for { rollbackBlock <- mkRollbackBlock(targetHash) _ = logger.info(s"Starting rollback to $targetHash using rollback block ${rollbackBlock.hash}") - fixedFinalizedBlock = if (finalizedBlock.height > rollbackBlock.parentBlock.height) rollbackBlock.parentBlock else finalizedBlock + fixedFinalizedBlock = if (finalizedBlock.height > rollbackBlock.parentPayload.height) rollbackBlock.parentPayload else finalizedBlock _ <- confirmBlock(rollbackBlock.hash, fixedFinalizedBlock.hash) _ <- confirmBlock(target, fixedFinalizedBlock) - lastEcBlock <- engineApiClient.getLastExecutionBlock + lastPayload <- engineApiClient.getLatestBlock _ <- Either.cond( - targetHash == lastEcBlock.hash, + targetHash == lastPayload.hash, (), - ClientError(s"Rollback to $targetHash error: last execution block ${lastEcBlock.hash} is not equal to target block hash") + ClientError(s"Rollback to $targetHash error: last block hash ${lastPayload.hash} is not equal to target block hash") ) } yield { logger.info(s"Rollback to $targetHash finished successfully") - val updatedLastValidatedBlock = if (lastEcBlock.height < prevState.fullValidationStatus.lastValidatedBlock.height) { - chainContractClient.getBlock(lastEcBlock.hash).getOrElse(finalizedBlock) + val updatedLastValidatedBlock = if (lastPayload.height < prevState.fullValidationStatus.lastValidatedBlock.height) { + chainContractClient.getBlock(lastPayload.hash).getOrElse(finalizedBlock) } else { prevState.fullValidationStatus.lastValidatedBlock } val newState = prevState.copy( - lastEcBlock = lastEcBlock, + lastPayload = lastPayload, fullValidationStatus = FullValidationStatus(updatedLastValidatedBlock, None) ) setState("10", newState) @@ -290,7 +280,7 @@ class ELUpdater( private def startBuildingPayload( epochInfo: EpochInfo, - parentBlock: EcBlock, + parentPayload: ExecutionPayload, finalizedBlock: ContractBlock, nextBlockUnixTs: Long, lastC2ETransferIndex: Long, @@ -317,16 +307,16 @@ class ELUpdater( val withdrawals = rewardWithdrawal ++ transferWithdrawals confirmBlockAndStartMining( - parentBlock, + parentPayload, finalizedBlock, nextBlockUnixTs, epochInfo.rewardAddress, - calculateRandao(epochInfo.hitSource, parentBlock.hash), + calculateRandao(epochInfo.hitSource, parentPayload.hash), withdrawals ).map { payloadId => logger.info( - s"Starting to forge payload $payloadId by miner ${epochInfo.miner} at height ${parentBlock.height + 1} " + - s"of epoch ${epochInfo.number} (ref=${parentBlock.hash}), ${withdrawals.size} withdrawals, ${transfers.size} transfers from $startC2ETransferIndex" + s"Starting to forge payload $payloadId by miner ${epochInfo.miner} at height ${parentPayload.height + 1} " + + s"of epoch ${epochInfo.number} (ref=${parentPayload.hash}), ${withdrawals.size} withdrawals, ${transfers.size} transfers from $startC2ETransferIndex" ) MiningData(payloadId, nextBlockUnixTs, transfers.lastOption.fold(lastC2ETransferIndex)(_.index), lastElWithdrawalIndex + withdrawals.size) @@ -334,8 +324,8 @@ class ELUpdater( } private def tryToStartMining(prevState: Working[ChainStatus], nodeChainInfo: Either[ChainSwitchInfo, ChainInfo]): Unit = { - val parentBlock = prevState.lastEcBlock - val epochInfo = prevState.epochInfo + val parentPayload = prevState.lastPayload + val epochInfo = prevState.epochInfo wallet.privateKeyAccount(epochInfo.miner) match { case Right(keyPair) if config.miningEnable => @@ -348,27 +338,27 @@ class ELUpdater( (for { elWithdrawalIndexBefore <- - parentBlock.withdrawals.lastOption.map(_.index) match { + parentPayload.withdrawals.lastOption.map(_.index) match { case Some(r) => Right(r) case None => - if (parentBlock.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) - else getLastWithdrawalIndex(parentBlock.parentHash) + if (parentPayload.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) + else getLastWithdrawalIndex(parentPayload.parentHash) } - nextBlockUnixTs = (parentBlock.timestamp + config.blockDelay.toSeconds).max(time.correctedTime() / 1000 + config.blockDelay.toSeconds) + nextBlockUnixTs = (parentPayload.timestamp + config.blockDelay.toSeconds).max(time.correctedTime() / 1000 + config.blockDelay.toSeconds) miningData <- startBuildingPayload( epochInfo, - parentBlock, + parentPayload, prevState.finalizedBlock, nextBlockUnixTs, lastC2ETransferIndex, elWithdrawalIndexBefore, prevState.options, - Option.unless(parentBlock.height == EthereumConstants.GenesisBlockHeight)(parentBlock.minerRewardL2Address) + Option.unless(parentPayload.height == EthereumConstants.GenesisBlockHeight)(parentPayload.feeRecipient) ) } yield { val newState = prevState.copy( epochInfo = epochInfo, - lastEcBlock = parentBlock, + lastPayload = parentPayload, chainStatus = Mining(keyPair, miningData.payloadId, nodeChainInfo, miningData.lastC2ETransferIndex, miningData.lastElWithdrawalIndex) ) @@ -376,9 +366,9 @@ class ELUpdater( scheduler.scheduleOnce((miningData.nextBlockUnixTs - time.correctedTime() / 1000).min(1).seconds)( prepareAndApplyPayload( miningData.payloadId, - parentBlock.hash, + parentPayload.hash, miningData.nextBlockUnixTs, - newState.options.startEpochChainFunction(parentBlock.hash, epochInfo.hitSource, nodeChainInfo.toOption), + newState.options.startEpochChainFunction(parentPayload.hash, epochInfo.hitSource, nodeChainInfo.toOption), newState.options ) ) @@ -393,16 +383,16 @@ class ELUpdater( private def tryToForgeNextBlock( epochNumber: Int, - parentBlock: EcBlock, + parentPayload: ExecutionPayload, chainContractOptions: ChainContractOptions ): Unit = { state match { case w @ Working(epochInfo, _, finalizedBlock, _, _, m: Mining, _, _) if epochInfo.number == epochNumber && blockchain.height == epochNumber => - val nextBlockUnixTs = (parentBlock.timestamp + config.blockDelay.toSeconds).max(time.correctedTime() / 1000) + val nextBlockUnixTs = (parentPayload.timestamp + config.blockDelay.toSeconds).max(time.correctedTime() / 1000) startBuildingPayload( epochInfo, - parentBlock, + parentPayload, finalizedBlock, nextBlockUnixTs, m.lastC2ETransferIndex, @@ -413,12 +403,12 @@ class ELUpdater( err => { logger.error(s"Error starting payload build process: ${err.message}") scheduler.scheduleOnce(MiningRetryInterval) { - tryToForgeNextBlock(epochNumber, parentBlock, chainContractOptions) + tryToForgeNextBlock(epochNumber, parentPayload, chainContractOptions) } }, miningData => { val newState = w.copy( - lastEcBlock = parentBlock, + lastPayload = parentPayload, chainStatus = m.copy( currentPayloadId = miningData.payloadId, lastC2ETransferIndex = miningData.lastC2ETransferIndex, @@ -429,15 +419,16 @@ class ELUpdater( scheduler.scheduleOnce((miningData.nextBlockUnixTs - time.correctedTime() / 1000).min(1).seconds)( prepareAndApplyPayload( miningData.payloadId, - parentBlock.hash, + parentPayload.hash, miningData.nextBlockUnixTs, - chainContractOptions.appendFunction(parentBlock.hash), + chainContractOptions.appendFunction(parentPayload.hash), chainContractOptions ) ) } ) - case other => logger.debug(s"Unexpected state $other attempting to start building block referencing ${parentBlock.hash} at epoch $epochNumber") + case other => + logger.debug(s"Unexpected state $other attempting to start building block referencing ${parentPayload.hash} at epoch $epochNumber") } } @@ -448,13 +439,13 @@ class ELUpdater( val finalizedBlock = chainContractClient.getFinalizedBlock logger.debug(s"Finalized block is ${finalizedBlock.hash}") engineApiClient.getBlockByHash(finalizedBlock.hash) match { - case Left(error) => logger.error(s"Could not load finalized block", error) - case Right(Some(finalizedEcBlock)) => - logger.trace(s"Finalized block ${finalizedBlock.hash} is at height ${finalizedEcBlock.height}") + case Left(error) => logger.error(s"Could not load finalized block payload", error) + case Right(Some(finalizedBlockPayload)) => + logger.trace(s"Finalized block ${finalizedBlock.hash} is at height ${finalizedBlockPayload.height}") (for { newEpochInfo <- calculateEpochInfo mainChainInfo <- chainContractClient.getMainChainInfo.toRight("Can't get main chain info") - lastEcBlock <- engineApiClient.getLastExecutionBlock.leftMap(_.message) + lastPayload <- engineApiClient.getLatestBlock.leftMap(_.message) } yield { logger.trace(s"Following main chain ${mainChainInfo.id}") val fullValidationStatus = FullValidationStatus( @@ -462,10 +453,10 @@ class ELUpdater( lastElWithdrawalIndex = None ) val options = chainContractClient.getOptions - followChainAndRequestNextBlock( + followChainAndRequestNextBlockPayload( newEpochInfo, mainChainInfo, - lastEcBlock, + lastPayload, mainChainInfo, finalizedBlock, fullValidationStatus, @@ -477,13 +468,14 @@ class ELUpdater( _ => () ) case Right(None) => - logger.trace(s"Finalized block ${finalizedBlock.hash} is not in EC, requesting from peers") - setState("15", WaitingForSyncHead(finalizedBlock, requestAndProcessBlock(finalizedBlock.hash))) + logger.trace(s"Finalized block ${finalizedBlock.hash} payload is not in EC, requesting from peers") + setState("15", WaitingForSyncHead(finalizedBlock, requestAndProcessBlockPayload(finalizedBlock.hash))) } } } private def handleConsensusLayerChanged(): Unit = { + payloadObserver.updateMinerPublicKeys(chainContractClient.getMinersPks) state match { case Starting => updateStartingState() case w: Working[ChainStatus] => updateWorkingState(w) @@ -491,7 +483,7 @@ class ELUpdater( } } - private def findAltChain(prevChainId: Long, referenceBlock: BlockHash) = { + private def findAltChain(prevChainId: Long, referenceBlock: BlockHash): Option[ChainInfo] = { logger.debug(s"Trying to find alternative chain referencing $referenceBlock") val lastChainId = chainContractClient.getLastChainId @@ -517,37 +509,37 @@ class ELUpdater( } } - private def requestBlocksAndStartMining(prevState: Working[FollowingChain]): Unit = { + private def requestPayloadsAndStartMining(prevState: Working[FollowingChain]): Unit = { def check(missedBlock: ContractBlock): Unit = { state match { - case w @ Working(epochInfo, lastEcBlock, finalizedBlock, mainChainInfo, _, fc: FollowingChain, _, returnToMainChainInfo) + case w @ Working(epochInfo, lastPayload, finalizedBlock, mainChainInfo, _, fc: FollowingChain, _, returnToMainChainInfo) if fc.nextExpectedBlock.map(_.hash).contains(missedBlock.hash) && canSupportAnotherAltChain(fc.nodeChainInfo) => - logger.debug(s"Block ${missedBlock.hash} wasn't received for $WaitRequestedBlockTimeout, need to switch to alternative chain") + logger.debug(s"Block ${missedBlock.hash} payload wasn't received for $WaitRequestedPayloadTimeout, need to switch to alternative chain") (for { lastValidBlock <- getAltChainReferenceBlock(fc.nodeChainInfo, missedBlock) updatedState <- rollbackTo(w, lastValidBlock, finalizedBlock) } yield { val updatedReturnToMainChainInfo = if (fc.nodeChainInfo.isMain) { - Some(ReturnToMainChainInfo(missedBlock, lastEcBlock, mainChainInfo.id)) + Some(ReturnToMainChainInfo(missedBlock, lastPayload, mainChainInfo.id)) } else returnToMainChainInfo findAltChain(fc.nodeChainInfo.id, lastValidBlock.hash) match { case Some(altChainInfo) => engineApiClient.getBlockByHash(finalizedBlock.hash) match { - case Right(Some(finalizedEcBlock)) => + case Right(Some(finalizedBlockPayload)) => followChainAndStartMining( updatedState.copy(chainStatus = FollowingChain(altChainInfo, None), returnToMainChainInfo = updatedReturnToMainChainInfo), epochInfo, altChainInfo.id, - finalizedEcBlock, + finalizedBlockPayload, finalizedBlock, chainContractClient.getOptions ) case Right(None) => - logger.warn(s"Finalized block ${finalizedBlock.hash} is not in EC") + logger.warn(s"Finalized block ${finalizedBlock.hash} payload is not in EC") case Left(err) => - logger.error(s"Could not load finalized block ${finalizedBlock.hash}", err) + logger.error(s"Could not load finalized block ${finalizedBlock.hash} payload", err) } case _ => val chainSwitchInfo = ChainSwitchInfo(fc.nodeChainInfo.id, lastValidBlock) @@ -564,8 +556,8 @@ class ELUpdater( case w: Working[ChainStatus] => w.chainStatus match { case FollowingChain(_, Some(nextExpectedBlock)) => - logger.debug(s"Waiting for block $nextExpectedBlock from peers") - scheduler.scheduleOnce(WaitRequestedBlockTimeout) { + logger.debug(s"Waiting for block $nextExpectedBlock payload from peers") + scheduler.scheduleOnce(WaitRequestedPayloadTimeout) { if (blockchain.height == prevState.epochInfo.number) { check(missedBlock) } @@ -582,7 +574,7 @@ class ELUpdater( prevState.chainStatus.nextExpectedBlock match { case Some(missedBlock) => - scheduler.scheduleOnce(WaitRequestedBlockTimeout) { + scheduler.scheduleOnce(WaitRequestedPayloadTimeout) { if (blockchain.height == prevState.epochInfo.number) { check(missedBlock) } @@ -596,7 +588,7 @@ class ELUpdater( prevState: Working[ChainStatus], newEpochInfo: EpochInfo, prevChainId: Long, - finalizedEcBlock: EcBlock, + finalizedBlockPayload: ExecutionPayload, finalizedBlock: ContractBlock, options: ChainContractOptions ): Unit = { @@ -604,11 +596,11 @@ class ELUpdater( prevState, newEpochInfo, prevChainId, - finalizedEcBlock, + finalizedBlockPayload, finalizedBlock, options ).foreach { newState => - requestBlocksAndStartMining(newState) + requestPayloadsAndStartMining(newState) } } @@ -643,9 +635,9 @@ class ELUpdater( val options = chainContractClient.getOptions logger.debug(s"Finalized block is ${finalizedBlock.hash}") engineApiClient.getBlockByHash(finalizedBlock.hash) match { - case Left(error) => logger.error(s"Could not load finalized block", error) - case Right(Some(finalizedEcBlock)) => - logger.trace(s"Finalized block ${finalizedBlock.hash} is at height ${finalizedEcBlock.height}") + case Left(error) => logger.error(s"Could not load finalized block payload", error) + case Right(Some(finalizedBlockPayload)) => + logger.trace(s"Finalized block ${finalizedBlock.hash} is at height ${finalizedBlockPayload.height}") if (blockchain.height != prevState.epochInfo.number || !blockchain.vrf(blockchain.height).contains(prevState.epochInfo.hitSource)) { calculateEpochInfo match { case Left(error) => @@ -658,7 +650,7 @@ class ELUpdater( prevState, newEpochInfo, nodeChainInfo.id, - finalizedEcBlock, + finalizedBlockPayload, finalizedBlock, options ) @@ -669,7 +661,7 @@ class ELUpdater( prevState, newEpochInfo, chainId, - finalizedEcBlock, + finalizedBlockPayload, finalizedBlock, options ) @@ -680,7 +672,7 @@ class ELUpdater( prevState, newEpochInfo, chainInfo.id, - finalizedEcBlock, + finalizedBlockPayload, finalizedBlock, options ) @@ -698,7 +690,7 @@ class ELUpdater( prevState, prevState.epochInfo, nodeChainInfo.id, - finalizedEcBlock, + finalizedBlockPayload, finalizedBlock, options ) @@ -706,22 +698,22 @@ class ELUpdater( case WaitForNewChain(chainSwitchInfo) => val newChainInfo = findAltChain(chainSwitchInfo.prevChainId, chainSwitchInfo.referenceBlock.hash) newChainInfo.foreach { chainInfo => - updateToFollowChain(prevState, prevState.epochInfo, chainInfo.id, finalizedEcBlock, finalizedBlock, options) + updateToFollowChain(prevState, prevState.epochInfo, chainInfo.id, finalizedBlockPayload, finalizedBlock, options) } } } validateAppliedBlocks() - requestMainChainBlock() + requestMainChainBlockPayload() case Right(None) => - logger.trace(s"Finalized block ${finalizedBlock.hash} is not in EC, requesting from peers") - setState("19", WaitingForSyncHead(finalizedBlock, requestAndProcessBlock(finalizedBlock.hash))) + logger.trace(s"Finalized block ${finalizedBlock.hash} payload is not in EC, requesting from peers") + setState("19", WaitingForSyncHead(finalizedBlock, requestAndProcessBlockPayload(finalizedBlock.hash))) } } - private def followChainAndRequestNextBlock( + private def followChainAndRequestNextBlockPayload( epochInfo: EpochInfo, nodeChainInfo: ChainInfo, - lastEcBlock: EcBlock, + lastPayload: ExecutionPayload, mainChainInfo: ChainInfo, finalizedBlock: ContractBlock, fullValidationStatus: FullValidationStatus, @@ -730,7 +722,7 @@ class ELUpdater( ): Working[FollowingChain] = { val newState = Working( epochInfo, - lastEcBlock, + lastPayload, finalizedBlock, mainChainInfo, fullValidationStatus, @@ -739,44 +731,44 @@ class ELUpdater( returnToMainChainInfo ) setState("3", newState) - maybeRequestNextBlock(newState, finalizedBlock) + maybeRequestNextBlockPayload(newState, finalizedBlock) } - private def requestBlock(contractBlock: ContractBlock): BlockRequestResult = { - logger.debug(s"Requesting block ${contractBlock.hash}") + private def requestBlockPayload(contractBlock: ContractBlock): PayloadRequestResult = { + logger.debug(s"Requesting payload for block ${contractBlock.hash}") engineApiClient.getBlockByHash(contractBlock.hash) match { - case Right(Some(block)) => BlockRequestResult.BlockExists(block) + case Right(Some(payload)) => PayloadRequestResult.Exists(payload) case Right(None) => - requestAndProcessBlock(contractBlock.hash) - BlockRequestResult.Requested(contractBlock) + requestAndProcessBlockPayload(contractBlock.hash) + PayloadRequestResult.Requested(contractBlock) case Left(err) => - logger.warn(s"Failed to get block ${contractBlock.hash} by hash: ${err.message}") - requestAndProcessBlock(contractBlock.hash) - BlockRequestResult.Requested(contractBlock) + logger.warn(s"Failed to get block ${contractBlock.hash} payload by hash: ${err.message}") + requestAndProcessBlockPayload(contractBlock.hash) + PayloadRequestResult.Requested(contractBlock) } } - private def requestMainChainBlock(): Unit = { + private def requestMainChainBlockPayload(): Unit = { state match { case w: Working[ChainStatus] => w.returnToMainChainInfo.foreach { returnToMainChainInfo => if (w.mainChainInfo.id == returnToMainChainInfo.chainId) { - requestBlock(returnToMainChainInfo.missedBlock) match { - case BlockRequestResult.BlockExists(block) => - logger.debug(s"Block ${returnToMainChainInfo.missedBlock.hash} exists at execution chain, trying to validate") - validateAppliedBlock(returnToMainChainInfo.missedBlock, block, w) match { + requestBlockPayload(returnToMainChainInfo.missedBlock) match { + case PayloadRequestResult.Exists(payload) => + logger.debug(s"Block ${returnToMainChainInfo.missedBlock.hash} payload exists at execution chain, trying to validate") + validateAppliedBlock(returnToMainChainInfo.missedBlock, payload, w) match { case Right(updatedState) => - logger.debug(s"Missed block ${block.hash} of main chain ${returnToMainChainInfo.chainId} was successfully validated") + logger.debug(s"Missed block ${payload.hash} of main chain ${returnToMainChainInfo.chainId} was successfully validated") chainContractClient.getChainInfo(returnToMainChainInfo.chainId) match { case Some(mainChainInfo) => - confirmBlockAndFollowChain(block, updatedState, mainChainInfo, None) + confirmBlockAndFollowChain(payload, updatedState, mainChainInfo, None) case _ => logger.error(s"Failed to get chain ${returnToMainChainInfo.chainId} info: not found") } case Left(err) => - logger.debug(s"Missed block ${block.hash} of main chain ${returnToMainChainInfo.chainId} validation error: ${err.message}") + logger.debug(s"Missed block ${payload.hash} of main chain ${returnToMainChainInfo.chainId} validation error: ${err.message}") } - case BlockRequestResult.Requested(_) => + case PayloadRequestResult.Requested(_) => } } } @@ -784,48 +776,50 @@ class ELUpdater( } } - private def requestAndProcessBlock(hash: BlockHash): CancelableFuture[(Channel, NetworkL2Block)] = { - requestBlockFromPeers(hash).andThen { - case Success((ch, block)) => executionBlockReceived(block, ch) - case Failure(exception) => logger.error(s"Error loading block $hash", exception) - }(globalScheduler) + private def requestAndProcessBlockPayload(hash: BlockHash): CancelableFuture[ExecutionPayloadInfo] = { + payloadObserver + .loadPayload(hash) + .andThen { + case Success(epi) => executionPayloadReceived(epi) + case Failure(exception) => logger.error(s"Error loading block $hash payload", exception) + }(globalScheduler) } private def updateToFollowChain( prevState: Working[ChainStatus], epochInfo: EpochInfo, prevChainId: Long, - finalizedEcBlock: EcBlock, + finalizedBlockPayload: ExecutionPayload, finalizedContractBlock: ContractBlock, options: ChainContractOptions ): Option[Working[FollowingChain]] = { @tailrec - def findLastEcBlock(curBlock: ContractBlock): EcBlock = { + def findLastPayload(curBlock: ContractBlock): ExecutionPayload = { engineApiClient.getBlockByHash(curBlock.hash) match { - case Right(Some(block)) => block + case Right(Some(payload)) => payload case Right(_) => chainContractClient.getBlock(curBlock.parentHash) match { - case Some(parent) => findLastEcBlock(parent) + case Some(parent) => findLastPayload(parent) case _ => logger.warn(s"Block ${curBlock.parentHash} not found at contract") - finalizedEcBlock + finalizedBlockPayload } case Left(err) => - logger.warn(s"Failed to get block ${curBlock.hash} by hash: ${err.message}") - finalizedEcBlock + logger.warn(s"Failed to get block ${curBlock.hash} payload by hash: ${err.message}") + finalizedBlockPayload } } def followChain( nodeChainInfo: ChainInfo, - lastEcBlock: EcBlock, + lastPayload: ExecutionPayload, mainChainInfo: ChainInfo, fullValidationStatus: FullValidationStatus, returnToMainChainInfo: Option[ReturnToMainChainInfo] ): Working[FollowingChain] = { val newState = Working( epochInfo, - lastEcBlock, + lastPayload, finalizedContractBlock, mainChainInfo, fullValidationStatus, @@ -834,32 +828,32 @@ class ELUpdater( returnToMainChainInfo.filter(rInfo => rInfo.chainId != prevChainId && mainChainInfo.id == rInfo.chainId) ) setState("16", newState) - maybeRequestNextBlock(newState, finalizedContractBlock) + maybeRequestNextBlockPayload(newState, finalizedContractBlock) } def rollbackAndFollowChain( - target: L2BlockLike, + target: CommonBlockData, nodeChainInfo: ChainInfo, mainChainInfo: ChainInfo, returnToMainChainInfo: Option[ReturnToMainChainInfo] ): Option[Working[FollowingChain]] = { rollbackTo(prevState, target, finalizedContractBlock) match { case Right(updatedState) => - Some(followChain(nodeChainInfo, updatedState.lastEcBlock, mainChainInfo, updatedState.fullValidationStatus, returnToMainChainInfo)) + Some(followChain(nodeChainInfo, updatedState.lastPayload, mainChainInfo, updatedState.fullValidationStatus, returnToMainChainInfo)) case Left(err) => logger.error(s"Failed to rollback to ${target.hash}: ${err.message}") None } } - def rollbackAndFollowMainChain(target: L2BlockLike, mainChainInfo: ChainInfo): Option[Working[FollowingChain]] = + def rollbackAndFollowMainChain(target: CommonBlockData, mainChainInfo: ChainInfo): Option[Working[FollowingChain]] = rollbackAndFollowChain(target, mainChainInfo, mainChainInfo, None) (chainContractClient.getMainChainInfo, chainContractClient.getChainInfo(prevChainId)) match { case (Some(mainChainInfo), Some(prevChainInfo)) => if (mainChainInfo.id != prevState.mainChainInfo.id) { - val updatedLastEcBlock = findLastEcBlock(mainChainInfo.lastBlock) - rollbackAndFollowMainChain(updatedLastEcBlock, mainChainInfo) + val updatedLastPayload = findLastPayload(mainChainInfo.lastBlock) + rollbackAndFollowMainChain(updatedLastPayload, mainChainInfo) } else if (prevChainInfo.firstBlock.height < finalizedContractBlock.height && !prevChainInfo.isMain) { val targetBlockHash = prevChainInfo.firstBlock.parentHash chainContractClient.getBlock(targetBlockHash) match { @@ -868,33 +862,33 @@ class ELUpdater( logger.error(s"Failed to get block $targetBlockHash meta at contract") None } - } else if (isLastEcBlockOnFork(prevChainInfo, prevState.lastEcBlock)) { - val updatedLastEcBlock = findLastEcBlock(prevChainInfo.lastBlock) - rollbackAndFollowChain(updatedLastEcBlock, prevChainInfo, mainChainInfo, prevState.returnToMainChainInfo) + } else if (isLastBlockOnFork(prevChainInfo, prevState.lastPayload)) { + val updatedLastPayload = findLastPayload(prevChainInfo.lastBlock) + rollbackAndFollowChain(updatedLastPayload, prevChainInfo, mainChainInfo, prevState.returnToMainChainInfo) } else { - Some(followChain(prevChainInfo, prevState.lastEcBlock, mainChainInfo, prevState.fullValidationStatus, prevState.returnToMainChainInfo)) + Some(followChain(prevChainInfo, prevState.lastPayload, mainChainInfo, prevState.fullValidationStatus, prevState.returnToMainChainInfo)) } case (Some(mainChainInfo), None) => - rollbackAndFollowMainChain(finalizedEcBlock, mainChainInfo) + rollbackAndFollowMainChain(finalizedBlockPayload, mainChainInfo) case (None, _) => logger.error("Failed to get main chain info") None } } - private def isLastEcBlockOnFork(chainInfo: ChainInfo, lastEcBlock: EcBlock) = - chainInfo.lastBlock.height == lastEcBlock.height && chainInfo.lastBlock.hash != lastEcBlock.hash || - chainInfo.lastBlock.height > lastEcBlock.height && !chainContractClient.blockExists(lastEcBlock.hash) || - chainInfo.lastBlock.height < lastEcBlock.height + private def isLastBlockOnFork(chainInfo: ChainInfo, lastBlock: CommonBlockData) = + chainInfo.lastBlock.height == lastBlock.height && chainInfo.lastBlock.hash != lastBlock.hash || + chainInfo.lastBlock.height > lastBlock.height && !chainContractClient.blockExists(lastBlock.hash) || + chainInfo.lastBlock.height < lastBlock.height private def waitForSyncCompletion(target: ContractBlock): Unit = scheduler.scheduleOnce(5.seconds)(state match { case SyncingToFinalizedBlock(finalizedBlockHash) if finalizedBlockHash == target.hash => logger.debug(s"Checking if EL has synced to ${target.hash} on height ${target.height}") - engineApiClient.getLastExecutionBlock match { + engineApiClient.getLatestBlock match { case Left(error) => logger.error(s"Sync to ${target.hash} was not completed, error=${error.message}") setState("23", Starting) - case Right(lastBlock) if lastBlock.hash == target.hash => + case Right(lastPayload) if lastPayload.hash == target.hash => logger.debug(s"Finished synchronization to ${target.hash} successfully") calculateEpochInfo match { case Left(err) => @@ -909,10 +903,10 @@ class ELUpdater( lastValidatedBlock = target, lastElWithdrawalIndex = None ) - followChainAndRequestNextBlock( + followChainAndRequestNextBlockPayload( newEpochInfo, mainChainInfo, - lastBlock, + lastPayload, mainChainInfo, target, fullValidationStatus, @@ -924,71 +918,60 @@ class ELUpdater( setState("25", Starting) } } - case Right(lastBlock) => - logger.debug(s"Sync to ${target.hash} is in progress: current last block is ${lastBlock.hash} at height ${lastBlock.height}") + case Right(lastPayload) => + logger.debug(s"Sync to ${target.hash} is in progress: current last block is ${lastPayload.hash} at height ${lastPayload.height}") waitForSyncCompletion(target) } case other => logger.debug(s"Unexpected state on sync: $other") }) - private def validateRandao(block: EcBlock, epochNumber: Int): JobResult[Unit] = + private def validateRandao(payload: ExecutionPayload, epochNumber: Int): JobResult[Unit] = blockchain.vrf(epochNumber) match { case None => ClientError(s"VRF of $epochNumber epoch is empty").asLeft case Some(vrf) => - val expectedPrevRandao = calculateRandao(vrf, block.parentHash) + val expectedPrevRandao = calculateRandao(vrf, payload.parentHash) Either.cond( - expectedPrevRandao == block.prevRandao, + expectedPrevRandao == payload.prevRandao, (), - ClientError(s"expected prevRandao $expectedPrevRandao, got ${block.prevRandao}, VRF=$vrf of $epochNumber") + ClientError(s"expected prevRandao $expectedPrevRandao, got ${payload.prevRandao}, VRF=$vrf of $epochNumber") ) } - private def validateMiner(block: NetworkL2Block, epochInfo: Option[EpochInfo]): JobResult[Unit] = { + private def validateMiner(epi: ExecutionPayloadInfo, epochInfo: Option[EpochInfo]): JobResult[Unit] = { + val payload = epi.payload epochInfo match { case Some(epochMeta) => - for { - _ <- Either.cond( - block.minerRewardL2Address == epochMeta.rewardAddress, - (), - ClientError(s"block miner ${block.minerRewardL2Address} doesn't equal to ${epochMeta.rewardAddress}") - ) - signature <- Either.fromOption(block.signature, ClientError(s"signature not found")) - publicKey <- Either.fromOption( - chainContractClient.getMinerPublicKey(block.minerRewardL2Address), - ClientError(s"public key for block miner ${block.minerRewardL2Address} not found") - ) - _ <- Either.cond( - crypto.verify(signature, Json.toBytes(block.payload), publicKey, checkWeakPk = true), - (), - ClientError(s"invalid signature") - ) - } yield () + Either.cond( + payload.feeRecipient == epochMeta.rewardAddress, + (), + ClientError(s"block miner ${payload.feeRecipient} doesn't equal to ${epochMeta.rewardAddress}") + ) case _ => Either.unit } } - private def validateTimestamp(newNetworkBlock: NetworkL2Block, parentEcBlock: EcBlock): JobResult[Unit] = { - val minAppendTs = parentEcBlock.timestamp + config.blockDelay.toSeconds + private def validateTimestamp(payload: ExecutionPayload, parentPayload: ExecutionPayload): JobResult[Unit] = { + val minAppendTs = parentPayload.timestamp + config.blockDelay.toSeconds Either.cond( - newNetworkBlock.timestamp >= minAppendTs, + payload.timestamp >= minAppendTs, (), ClientError( - s"timestamp (${newNetworkBlock.timestamp}) of appended block must be greater or equal $minAppendTs, " + - s"Δ${minAppendTs - newNetworkBlock.timestamp}s" + s"timestamp (${payload.timestamp}) of appended block must be greater or equal $minAppendTs, " + + s"Δ${minAppendTs - payload.timestamp}s" ) ) } private def preValidateBlock( - networkBlock: NetworkL2Block, - parentBlock: EcBlock, + epi: ExecutionPayloadInfo, + parentPayload: ExecutionPayload, epochInfo: Option[EpochInfo] ): JobResult[Unit] = { for { - _ <- validateTimestamp(networkBlock, parentBlock) - _ <- validateMiner(networkBlock, epochInfo) - _ <- engineApiClient.applyNewPayload(networkBlock.payload) + _ <- validateTimestamp(epi.payload, parentPayload) + _ <- validateMiner(epi, epochInfo) + _ <- engineApiClient.applyNewPayload(epi.payloadJson) } yield () } @@ -1011,81 +994,81 @@ class ELUpdater( .toRight(ClientError(s"Can't find a last block $referenceBlockHash of epoch #${lastEpoch.prevEpoch} on contract")) } yield referenceBlock } else { - val blockId = nodeChainInfo.firstBlock.parentHash + val blockHash = nodeChainInfo.firstBlock.parentHash chainContractClient - .getBlock(blockId) + .getBlock(blockHash) .toRight( - ClientError(s"Parent block $blockId for first block ${nodeChainInfo.firstBlock.hash} of chain ${nodeChainInfo.id} not found at contract") + ClientError(s"Parent block $blockHash for first block ${nodeChainInfo.firstBlock.hash} of chain ${nodeChainInfo.id} not found at contract") ) } } - private def validateAndApplyMissedBlock( - networkBlock: NetworkL2Block, - ch: Channel, + private def validateAndApplyMissed( + epi: ExecutionPayloadInfo, prevState: Working[ChainStatus], contractBlock: ContractBlock, - parentBlock: EcBlock, + parentPayload: ExecutionPayload, nodeChainInfo: ChainInfo ): Unit = { - validateBlockFull(networkBlock, contractBlock, parentBlock, prevState) match { + val payload = epi.payload + validateBlockFull(epi, contractBlock, parentPayload, prevState) match { case Right(updatedState) => - logger.debug(s"Missed block ${networkBlock.hash} of main chain ${nodeChainInfo.id} was successfully validated") - broadcastAndConfirmBlock(networkBlock, ch, updatedState, nodeChainInfo, None) + logger.debug(s"Missed block ${payload.hash} of main chain ${nodeChainInfo.id} was successfully validated") + broadcastAndConfirmBlock(epi, updatedState, nodeChainInfo, None) case Left(err) => - logger.debug(s"Missed block ${networkBlock.hash} of main chain ${nodeChainInfo.id} validation error: ${err.message}, ignoring block") + logger.debug(s"Missed block ${payload.hash} of main chain ${nodeChainInfo.id} validation error: ${err.message}, ignoring block") } } private def validateAndApply( - networkBlock: NetworkL2Block, - ch: Channel, + epi: ExecutionPayloadInfo, prevState: Working[ChainStatus], - parentBlock: EcBlock, + parentPayload: ExecutionPayload, nodeChainInfo: ChainInfo, returnToMainChainInfo: Option[ReturnToMainChainInfo] ): Unit = { - chainContractClient.getBlock(networkBlock.hash) match { - case Some(contractBlock) if prevState.fullValidationStatus.lastValidatedBlock.hash == parentBlock.hash => + val payload = epi.payload + chainContractClient.getBlock(payload.hash) match { + case Some(contractBlock) if prevState.fullValidationStatus.lastValidatedBlock.hash == parentPayload.hash => // all blocks before current was fully validated, so we can perform full validation of this block - validateBlockFull(networkBlock, contractBlock, parentBlock, prevState) match { + validateBlockFull(epi, contractBlock, parentPayload, prevState) match { case Right(updatedState) => - logger.debug(s"Block ${networkBlock.hash} was successfully validated") - broadcastAndConfirmBlock(networkBlock, ch, updatedState, nodeChainInfo, returnToMainChainInfo) + logger.debug(s"Block ${payload.hash} was successfully validated") + broadcastAndConfirmBlock(epi, updatedState, nodeChainInfo, returnToMainChainInfo) case Left(err) => - logger.debug(s"Block ${networkBlock.hash} validation error: ${err.message}") + logger.debug(s"Block ${payload.hash} validation error: ${err.message}") processInvalidBlock(contractBlock, prevState, Some(nodeChainInfo)) } case contractBlock => // we should check block miner based on epochInfo if block is not at contract yet val epochInfo = if (contractBlock.isEmpty) Some(prevState.epochInfo) else None - preValidateBlock(networkBlock, parentBlock, epochInfo) match { + preValidateBlock(epi, parentPayload, epochInfo) match { case Right(_) => - logger.debug(s"Block ${networkBlock.hash} was successfully partially validated") - broadcastAndConfirmBlock(networkBlock, ch, prevState, nodeChainInfo, returnToMainChainInfo) + logger.debug(s"Block ${payload.hash} was successfully partially validated") + broadcastAndConfirmBlock(epi, prevState, nodeChainInfo, returnToMainChainInfo) case Left(err) => - logger.error(s"Block ${networkBlock.hash} prevalidation error: ${err.message}, ignoring block") + logger.error(s"Block ${payload.hash} prevalidation error: ${err.message}, ignoring block") } } } private def confirmBlockAndFollowChain( - block: EcBlock, + payload: ExecutionPayload, prevState: Working[ChainStatus], nodeChainInfo: ChainInfo, returnToMainChainInfo: Option[ReturnToMainChainInfo] ): Unit = { val finalizedBlock = prevState.finalizedBlock - confirmBlock(block, finalizedBlock) + confirmBlock(payload, finalizedBlock) .fold[Unit]( - err => logger.error(s"Can't confirm block ${block.hash} of chain ${nodeChainInfo.id}: ${err.message}"), + err => logger.error(s"Can't confirm block ${payload.hash} of chain ${nodeChainInfo.id}: ${err.message}"), _ => { - logger.info(s"Successfully confirmed block ${block.hash} of chain ${nodeChainInfo.id}") - followChainAndRequestNextBlock( + logger.info(s"Successfully confirmed block ${payload.hash} of chain ${nodeChainInfo.id}") + followChainAndRequestNextBlockPayload( prevState.epochInfo, nodeChainInfo, - block, + payload, prevState.mainChainInfo, finalizedBlock, prevState.fullValidationStatus, @@ -1098,17 +1081,13 @@ class ELUpdater( } private def broadcastAndConfirmBlock( - networkBlock: NetworkL2Block, - ch: Channel, + epi: ExecutionPayloadInfo, prevState: Working[ChainStatus], nodeChainInfo: ChainInfo, returnToMainChainInfo: Option[ReturnToMainChainInfo] ): Unit = { - Try(allChannels.broadcast(networkBlock, Some(ch))).recover { err => - logger.error(s"Failed to broadcast block ${networkBlock.hash}: ${err.getMessage}") - } - - confirmBlockAndFollowChain(networkBlock.toEcBlock, prevState, nodeChainInfo, returnToMainChainInfo) + payloadObserver.broadcast(epi.payload.hash) + confirmBlockAndFollowChain(epi.payload, prevState, nodeChainInfo, returnToMainChainInfo) } private def findBlockChild(parent: BlockHash, lastBlockHash: BlockHash): Either[String, ContractBlock] = { @@ -1124,53 +1103,55 @@ class ELUpdater( } @tailrec - private def maybeRequestNextBlock(prevState: Working[FollowingChain], finalizedBlock: ContractBlock): Working[FollowingChain] = { - if (prevState.lastEcBlock.height < prevState.chainStatus.nodeChainInfo.lastBlock.height) { - logger.debug(s"EC chain is not synced, trying to find next block to request") - findBlockChild(prevState.lastEcBlock.hash, prevState.chainStatus.nodeChainInfo.lastBlock.hash) match { + private def maybeRequestNextBlockPayload(prevState: Working[FollowingChain], finalizedBlock: ContractBlock): Working[FollowingChain] = { + if (prevState.lastPayload.height < prevState.chainStatus.nodeChainInfo.lastBlock.height) { + logger.debug(s"EC chain is not synced, trying to find next block to request payload") + findBlockChild(prevState.lastPayload.hash, prevState.chainStatus.nodeChainInfo.lastBlock.hash) match { case Left(error) => - logger.error(s"Could not find child of ${prevState.lastEcBlock.hash} on contract: $error") + logger.error(s"Could not find child of ${prevState.lastPayload.hash} on contract: $error") prevState case Right(contractBlock) => - requestBlock(contractBlock) match { - case BlockRequestResult.BlockExists(ecBlock) => - logger.debug(s"Block ${contractBlock.hash} exists at EC chain, trying to confirm") - confirmBlock(ecBlock, finalizedBlock) match { + requestBlockPayload(contractBlock) match { + case PayloadRequestResult.Exists(payload) => + logger.debug(s"Block ${contractBlock.hash} payload exists at EC chain, trying to confirm") + confirmBlock(payload, finalizedBlock) match { case Right(_) => val newState = prevState.copy( - lastEcBlock = ecBlock, + lastPayload = payload, chainStatus = FollowingChain(prevState.chainStatus.nodeChainInfo, None) ) setState("7", newState) - maybeRequestNextBlock(newState, finalizedBlock) + maybeRequestNextBlockPayload(newState, finalizedBlock) case Left(err) => - logger.error(s"Failed to confirm next block ${ecBlock.hash}: ${err.message}") + logger.error(s"Failed to confirm next block ${payload.hash}: ${err.message}") prevState } - case BlockRequestResult.Requested(contractBlock) => + case PayloadRequestResult.Requested(contractBlock) => val newState = prevState.copy(chainStatus = prevState.chainStatus.copy(nextExpectedBlock = Some(contractBlock))) setState("8", newState) newState } } } else { - logger.trace(s"EC chain ${prevState.chainStatus.nodeChainInfo.id} is synced, no need to request blocks") + logger.trace(s"EC chain ${prevState.chainStatus.nodeChainInfo.id} is synced, no need to request block payloads") prevState } } - private def mkRollbackBlock(rollbackTargetBlockId: BlockHash): JobResult[RollbackBlock] = for { - targetBlockFromContract <- Right(chainContractClient.getBlock(rollbackTargetBlockId)) - targetBlockOpt <- targetBlockFromContract match { - case None => engineApiClient.getBlockByHash(rollbackTargetBlockId) + private def mkRollbackBlock(rollbackTargetBlockHash: BlockHash): JobResult[RollbackBlock] = for { + targetBlockDataOpt <- chainContractClient.getBlock(rollbackTargetBlockHash) match { + case None => engineApiClient.getBlockByHash(rollbackTargetBlockHash) case x => Right(x) } - targetBlock <- Either.fromOption(targetBlockOpt, ClientError(s"Can't find block $rollbackTargetBlockId neither on a contract, nor in EC")) - parentBlock <- engineApiClient.getBlockByHash(targetBlock.parentHash) - parentBlock <- Either.fromOption(parentBlock, ClientError(s"Can't find parent block $rollbackTargetBlockId in execution client")) - rollbackBlockOpt <- engineApiClient.applyNewPayload(EmptyL2Block.mkExecutionPayload(parentBlock)) + targetBlockData <- Either.fromOption( + targetBlockDataOpt, + ClientError(s"Can't find block $rollbackTargetBlockHash neither on a contract, nor in EC") + ) + parentPayloadOpt <- engineApiClient.getBlockByHash(targetBlockData.parentHash) + parentPayload <- Either.fromOption(parentPayloadOpt, ClientError(s"Can't find block $rollbackTargetBlockHash parent payload in execution client")) + rollbackBlockOpt <- engineApiClient.applyNewPayload(EmptyPayload.mkExecutionPayloadJson(parentPayload)) rollbackBlock <- Either.fromOption(rollbackBlockOpt, ClientError("Rollback block hash is not defined as latest valid hash")) - } yield RollbackBlock(rollbackBlock, parentBlock) + } yield RollbackBlock(rollbackBlock, parentPayload) private def toWithdrawals(transfers: Vector[ChainContractClient.ContractTransfer], firstWithdrawalIndex: Long): Vector[Withdrawal] = transfers.zipWithIndex.map { case (x, i) => @@ -1180,13 +1161,13 @@ class ELUpdater( private def getLastWithdrawalIndex(hash: BlockHash): JobResult[WithdrawalIndex] = engineApiClient.getBlockByHash(hash).flatMap { - case None => Left(ClientError(s"Can't find $hash block on EC during withdrawal search")) - case Some(ecBlock) => - ecBlock.withdrawals.lastOption match { + case None => Left(ClientError(s"Can't find block $hash payload on EC during withdrawal search")) + case Some(payload) => + payload.withdrawals.lastOption match { case Some(lastWithdrawal) => Right(lastWithdrawal.index) case None => - if (ecBlock.height == 0) Right(-1L) - else getLastWithdrawalIndex(ecBlock.parentHash) + if (payload.height == 0) Right(-1L) + else getLastWithdrawalIndex(payload.parentHash) } } @@ -1208,7 +1189,7 @@ class ELUpdater( } } yield rootHash - private def skipFinalizedBlocksValidation(curState: Working[ChainStatus]) = { + private def skipFinalizedBlocksValidation(curState: Working[ChainStatus]): Working[ChainStatus] = { if (curState.finalizedBlock.height > curState.fullValidationStatus.lastValidatedBlock.height) { val newState = curState.copy(fullValidationStatus = FullValidationStatus(curState.finalizedBlock, None)) setState("4", newState) @@ -1226,7 +1207,7 @@ class ELUpdater( blocksToValidate.foldLeft[JobResult[Working[ChainStatus]]](Right(startState)) { case (Right(curState), block) => logger.debug(s"Trying to validate applied block ${block.hash}") - validateAppliedBlock(block.contractBlock, block.ecBlock, curState) match { + validateAppliedBlock(block.contractBlock, block.payload, curState) match { case Right(updatedState) => logger.debug(s"Block ${block.hash} was successfully validated") Right(updatedState) @@ -1260,7 +1241,7 @@ class ELUpdater( private def validateWithdrawals( contractBlock: ContractBlock, - ecBlock: EcBlock, + payload: ExecutionPayload, fullValidationStatus: FullValidationStatus, chainContractOptions: ChainContractOptions ): JobResult[Option[WithdrawalIndex]] = { @@ -1279,14 +1260,14 @@ class ELUpdater( val prevMinerElRewardAddress = if (expectMiningReward) chainContractClient.getElRewardAddress(blockPrevEpoch.miner) else None for { - elWithdrawalIndexBefore <- fullValidationStatus.checkedLastElWithdrawalIndex(ecBlock.parentHash) match { + elWithdrawalIndexBefore <- fullValidationStatus.checkedLastElWithdrawalIndex(payload.parentHash) match { case Some(r) => Right(r) case None => - if (ecBlock.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) - else getLastWithdrawalIndex(ecBlock.parentHash) + if (payload.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) + else getLastWithdrawalIndex(payload.parentHash) } lastElWithdrawalIndex <- validateC2ETransfers( - ecBlock, + payload, contractBlock, prevMinerElRewardAddress, chainContractOptions, @@ -1297,35 +1278,37 @@ class ELUpdater( } private def validateBlockFull( - networkBlock: NetworkL2Block, + epi: ExecutionPayloadInfo, contractBlock: ContractBlock, - parentBlock: EcBlock, + parentPayload: ExecutionPayload, prevState: Working[ChainStatus] ): JobResult[Working[ChainStatus]] = { - logger.debug(s"Trying to do full validation of block ${networkBlock.hash}") + val payload = epi.payload + logger.debug(s"Trying to do full validation of block ${payload.hash}") for { - _ <- preValidateBlock(networkBlock, parentBlock, None) - ecBlock = networkBlock.toEcBlock - updatedState <- validateAppliedBlock(contractBlock, ecBlock, prevState) + _ <- preValidateBlock(epi, parentPayload, None) + updatedState <- validateAppliedBlock(contractBlock, payload, prevState) } yield updatedState } // Note: we can not do this validation before block application, because we need block logs private def validateAppliedBlock( contractBlock: ContractBlock, - ecBlock: EcBlock, + payload: ExecutionPayload, prevState: Working[ChainStatus] ): JobResult[Working[ChainStatus]] = { val validationResult = for { _ <- Either.cond( - contractBlock.minerRewardL2Address == ecBlock.minerRewardL2Address, + contractBlock.feeRecipient == payload.feeRecipient, (), - ClientError(s"Miner in EC block ${ecBlock.minerRewardL2Address} should be equal to miner on contract ${contractBlock.minerRewardL2Address}") + ClientError( + s"Miner in block payload (${payload.feeRecipient}) should be equal to miner on contract (${contractBlock.feeRecipient})" + ) ) _ <- validateE2CTransfers(contractBlock, prevState.options.elBridgeAddress) - updatedLastElWithdrawalIndex <- validateWithdrawals(contractBlock, ecBlock, prevState.fullValidationStatus, prevState.options) - _ <- validateRandao(ecBlock, contractBlock.epoch) + updatedLastElWithdrawalIndex <- validateWithdrawals(contractBlock, payload, prevState.fullValidationStatus, prevState.options) + _ <- validateRandao(payload, contractBlock.epoch) } yield updatedLastElWithdrawalIndex validationResult.map { lastElWithdrawalIndex => @@ -1351,8 +1334,8 @@ class ELUpdater( referenceBlock <- getAltChainReferenceBlock(chainInfo, contractBlock) updatedState <- rollbackTo(prevState, referenceBlock, prevState.finalizedBlock) lastValidBlock <- chainContractClient - .getBlock(updatedState.lastEcBlock.hash) - .toRight(ClientError(s"Block ${updatedState.lastEcBlock.hash} not found at contract")) + .getBlock(updatedState.lastPayload.hash) + .toRight(ClientError(s"Block ${updatedState.lastPayload.hash} not found at contract")) } yield { findAltChain(chainInfo.id, lastValidBlock.hash) match { case Some(altChainInfo) => @@ -1389,16 +1372,16 @@ class ELUpdater( } else { chainContractClient.getBlock(curBlock.parentHash) match { case Some(parentBlock) => - if (curBlock.height > curState.lastEcBlock.height) { + if (curBlock.height > curState.lastPayload.height) { loop(parentBlock, acc) } else { engineApiClient.getBlockByHash(curBlock.hash) match { - case Right(Some(ecBlock)) => - loop(parentBlock, BlockForValidation(curBlock, ecBlock) :: acc) + case Right(Some(payload)) => + loop(parentBlock, BlockForValidation(curBlock, payload) :: acc) case Right(None) => - Left(ClientError(s"Block ${curBlock.hash} not found on EC client for full validation")) + Left(ClientError(s"Block ${curBlock.hash} payload not found on EC client for full validation")) case Left(err) => - Left(ClientError(s"Can't get EC block ${curBlock.hash} for full validation: ${err.message}")) + Left(ClientError(s"Can't get block ${curBlock.hash} payload for full validation: ${err.message}")) } } case _ => @@ -1411,7 +1394,7 @@ class ELUpdater( } private def validateC2ETransfers( - ecBlock: EcBlock, + payload: ExecutionPayload, contractBlock: ContractBlock, prevMinerElRewardAddress: Option[EthAddress], options: ChainContractOptions, @@ -1430,23 +1413,23 @@ class ELUpdater( for { expectedWithdrawals <- prevMinerElRewardAddress match { case None => - if (ecBlock.withdrawals.size == expectedTransfers.size) toWithdrawals(expectedTransfers, firstWithdrawalIndex).asRight - else s"Expected ${expectedTransfers.size} withdrawals, got ${ecBlock.withdrawals.size}".asLeft + if (payload.withdrawals.size == expectedTransfers.size) toWithdrawals(expectedTransfers, firstWithdrawalIndex).asRight + else s"Expected ${expectedTransfers.size} withdrawals, got ${payload.withdrawals.size}".asLeft case Some(prevMinerElRewardAddress) => - if (ecBlock.withdrawals.size == expectedTransfers.size + 1) { // +1 for reward + if (payload.withdrawals.size == expectedTransfers.size + 1) { // +1 for reward val rewardWithdrawal = Withdrawal(firstWithdrawalIndex, prevMinerElRewardAddress, options.miningReward) val userWithdrawals = toWithdrawals(expectedTransfers, rewardWithdrawal.index + 1) (rewardWithdrawal +: userWithdrawals).asRight - } else s"Expected ${expectedTransfers.size + 1} (at least reward) withdrawals, got ${ecBlock.withdrawals.size}".asLeft + } else s"Expected ${expectedTransfers.size + 1} (at least reward) withdrawals, got ${payload.withdrawals.size}".asLeft } - _ <- validateC2ETransfers(ecBlock, expectedWithdrawals) + _ <- validateC2ETransfers(payload, expectedWithdrawals) } yield expectedWithdrawals.lastOption.fold(elWithdrawalIndexBefore)(_.index) } - private def validateC2ETransfers(ecBlock: EcBlock, expectedWithdrawals: Seq[Withdrawal]): Either[String, Unit] = - ecBlock.withdrawals + private def validateC2ETransfers(payload: ExecutionPayload, expectedWithdrawals: Seq[Withdrawal]): Either[String, Unit] = + payload.withdrawals .zip(expectedWithdrawals) .zipWithIndex .toList @@ -1471,26 +1454,26 @@ class ELUpdater( } .map(_ => ()) - private def confirmBlock(block: L2BlockLike, finalizedBlock: L2BlockLike): JobResult[PayloadStatus] = { - val finalizedBlockHash = if (finalizedBlock.height > block.height) block.hash else finalizedBlock.hash - engineApiClient.forkChoiceUpdate(block.hash, finalizedBlockHash) + private def confirmBlock(blockData: CommonBlockData, finalizedBlockData: CommonBlockData): JobResult[PayloadStatus] = { + val finalizedBlockHash = if (finalizedBlockData.height > blockData.height) blockData.hash else finalizedBlockData.hash + engineApiClient.forkChoiceUpdated(blockData.hash, finalizedBlockHash) } private def confirmBlock(hash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = - engineApiClient.forkChoiceUpdate(hash, finalizedBlockHash) + engineApiClient.forkChoiceUpdated(hash, finalizedBlockHash) private def confirmBlockAndStartMining( - lastBlock: EcBlock, + lastPayload: ExecutionPayload, finalizedBlock: ContractBlock, unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, withdrawals: Vector[Withdrawal] ): JobResult[PayloadId] = { - val finalizedBlockHash = if (finalizedBlock.height > lastBlock.height) lastBlock.hash else finalizedBlock.hash + val finalizedBlockHash = if (finalizedBlock.height > lastPayload.height) lastPayload.hash else finalizedBlock.hash engineApiClient - .forkChoiceUpdateWithPayloadId( - lastBlock.hash, + .forkChoiceUpdatedWithPayloadId( + lastPayload.hash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, @@ -1517,7 +1500,7 @@ object ELUpdater { val WaitForReferenceConfirmInterval: FiniteDuration = 500.millis val ClChangedProcessingDelay: FiniteDuration = 50.millis val MiningRetryInterval: FiniteDuration = 5.seconds - val WaitRequestedBlockTimeout: FiniteDuration = 2.seconds + val WaitRequestedPayloadTimeout: FiniteDuration = 2.seconds case class EpochInfo(number: Int, miner: Address, rewardAddress: EthAddress, hitSource: ByteStr, prevEpochLastBlockHash: Option[BlockHash]) @@ -1527,7 +1510,7 @@ object ELUpdater { case class Working[+CS <: ChainStatus]( epochInfo: EpochInfo, - lastEcBlock: EcBlock, + lastPayload: ExecutionPayload, finalizedBlock: ContractBlock, mainChainInfo: ChainInfo, fullValidationStatus: FullValidationStatus, @@ -1563,28 +1546,28 @@ object ELUpdater { } } - case class WaitingForSyncHead(target: ContractBlock, task: CancelableFuture[BlockWithChannel]) extends State - case class SyncingToFinalizedBlock(target: BlockHash) extends State + case class WaitingForSyncHead(target: ContractBlock, task: CancelableFuture[ExecutionPayloadInfo]) extends State + case class SyncingToFinalizedBlock(target: BlockHash) extends State } - private case class RollbackBlock(hash: BlockHash, parentBlock: EcBlock) + private case class RollbackBlock(hash: BlockHash, parentPayload: ExecutionPayload) case class ChainSwitchInfo(prevChainId: Long, referenceBlock: ContractBlock) - /** We haven't received a EC-block {@link missedBlock} of a previous epoch when started a mining on a new epoch. We can return to the main chain, if - * get a missed EC-block. + /** We haven't received block payload of a previous epoch when started a mining on a new epoch. We can return to the main chain, if get a missed + * block payload. */ - case class ReturnToMainChainInfo(missedBlock: ContractBlock, missedBlockParent: EcBlock, chainId: Long) + case class ReturnToMainChainInfo(missedBlock: ContractBlock, missedBlockParentPayload: ExecutionPayload, chainId: Long) - sealed trait BlockRequestResult - private object BlockRequestResult { - case class BlockExists(block: EcBlock) extends BlockRequestResult - case class Requested(contractBlock: ContractBlock) extends BlockRequestResult + sealed trait PayloadRequestResult + private object PayloadRequestResult { + case class Exists(payload: ExecutionPayload) extends PayloadRequestResult + case class Requested(contractBlock: ContractBlock) extends PayloadRequestResult } private case class MiningData(payloadId: PayloadId, nextBlockUnixTs: Long, lastC2ETransferIndex: Long, lastElWithdrawalIndex: WithdrawalIndex) - private case class BlockForValidation(contractBlock: ContractBlock, ecBlock: EcBlock) { + private case class BlockForValidation(contractBlock: ContractBlock, payload: ExecutionPayload) { val hash: BlockHash = contractBlock.hash } diff --git a/src/main/scala/units/ExecutionPayloadInfo.scala b/src/main/scala/units/ExecutionPayloadInfo.scala new file mode 100644 index 00000000..202a34e5 --- /dev/null +++ b/src/main/scala/units/ExecutionPayloadInfo.scala @@ -0,0 +1,6 @@ +package units + +import play.api.libs.json.JsObject +import units.client.engine.model.ExecutionPayload + +case class ExecutionPayloadInfo(payload: ExecutionPayload, payloadJson: JsObject) diff --git a/src/main/scala/units/NetworkL2Block.scala b/src/main/scala/units/NetworkL2Block.scala deleted file mode 100644 index 21b6d5eb..00000000 --- a/src/main/scala/units/NetworkL2Block.scala +++ /dev/null @@ -1,99 +0,0 @@ -package units - -import cats.syntax.either.* -import com.wavesplatform.account.PrivateKey -import com.wavesplatform.common.state.ByteStr -import com.wavesplatform.crypto -import com.wavesplatform.crypto.{DigestLength, SignatureLength} -import org.web3j.abi.datatypes.generated.Uint256 -import play.api.libs.json.{JsObject, Json} -import units.client.L2BlockLike -import units.client.engine.model.{EcBlock, Withdrawal} -import units.eth.EthAddress -import units.util.HexBytesConverter.* - -// TODO Refactor to eliminate a manual deserialization, e.g. (raw: JsonObject, parsed: ParsedBlockL2) -class NetworkL2Block private ( - val hash: BlockHash, - val timestamp: Long, // UNIX epoch seconds - val height: Long, - val parentHash: BlockHash, - val stateRoot: String, - val minerRewardL2Address: EthAddress, - val baseFeePerGas: Uint256, - val gasLimit: Long, - val gasUsed: Long, - val prevRandao: String, - val withdrawals: Vector[Withdrawal], - val payloadBytes: Array[Byte], - val payload: JsObject, - val signature: Option[ByteStr] -) extends L2BlockLike { - def isEpochFirstBlock: Boolean = withdrawals.nonEmpty - - def toEcBlock: EcBlock = EcBlock( - hash = hash, - parentHash = parentHash, - stateRoot = stateRoot, - height = height, - timestamp = timestamp, - minerRewardL2Address = minerRewardL2Address, - baseFeePerGas = baseFeePerGas, - gasLimit = gasLimit, - gasUsed = gasUsed, - prevRandao = prevRandao, - withdrawals = withdrawals - ) - - override def toString: String = s"NetworkL2Block($hash)" -} - -object NetworkL2Block { - private def apply(payload: JsObject, payloadBytes: Array[Byte], signature: Option[ByteStr]): Either[ClientError, NetworkL2Block] = { - // See BlockToPayloadMapper for all available fields - (for { - hash <- (payload \ "blockHash").asOpt[BlockHash].toRight("hash not defined") - timestamp <- (payload \ "timestamp").asOpt[String].map(toLong).toRight("timestamp not defined") - height <- (payload \ "blockNumber").asOpt[String].map(toLong).toRight("height not defined") - parentHash <- (payload \ "parentHash").asOpt[BlockHash].toRight("parent hash not defined") - stateRoot <- (payload \ "stateRoot").asOpt[String].toRight("state root not defined") - minerRewardL2Address <- (payload \ "feeRecipient").asOpt[EthAddress].toRight("fee recipient not defined") - baseFeePerGas <- (payload \ "baseFeePerGas").asOpt[String].map(toUint256).toRight("baseFeePerGas not defined") - gasLimit <- (payload \ "gasLimit").asOpt[String].map(toLong).toRight("gasLimit not defined") - gasUsed <- (payload \ "gasUsed").asOpt[String].map(toLong).toRight("gasUsed not defined") - prevRandao <- (payload \ "prevRandao").asOpt[String].toRight("prevRandao not defined") - withdrawals <- (payload \ "withdrawals").asOpt[Vector[Withdrawal]].toRight("withdrawals are not defined") - _ <- Either.cond(signature.forall(_.size == SignatureLength), (), "invalid signature size") - } yield new NetworkL2Block( - hash, - timestamp, - height, - parentHash, - stateRoot, - minerRewardL2Address, - baseFeePerGas, - gasLimit, - gasUsed, - prevRandao, - withdrawals, - payloadBytes, - payload, - signature - )).leftMap(err => ClientError(s"Error creating BlockL2 from payload ${new String(payloadBytes)}: $err at payload")) - } - - def apply(payloadBytes: Array[Byte], signature: Option[ByteStr]): Either[ClientError, NetworkL2Block] = for { - payload <- Json.parse(payloadBytes).asOpt[JsObject].toRight(ClientError("Payload is not a valid JSON object")) - block <- apply(payload, payloadBytes, signature) - } yield block - - def signed(payload: JsObject, signer: PrivateKey): Either[ClientError, NetworkL2Block] = { - val payloadBytes = Json.toBytes(payload) - NetworkL2Block(payload, payloadBytes, Some(crypto.sign(signer, payloadBytes))) - } - - def apply(payload: JsObject): Either[ClientError, NetworkL2Block] = apply(payload, Json.toBytes(payload), None) - - def validateReferenceLength(length: Int): Boolean = - length == DigestLength -} diff --git a/src/main/scala/units/client/CommonBlockData.scala b/src/main/scala/units/client/CommonBlockData.scala new file mode 100644 index 00000000..696ff792 --- /dev/null +++ b/src/main/scala/units/client/CommonBlockData.scala @@ -0,0 +1,13 @@ +package units.client + +import units.BlockHash +import units.eth.{EthAddress, EthereumConstants} + +trait CommonBlockData { + def hash: BlockHash + def parentHash: BlockHash + def height: Long + def feeRecipient: EthAddress + + def referencesGenesis: Boolean = height == EthereumConstants.GenesisBlockHeight + 1 +} diff --git a/src/main/scala/units/client/L2BlockLike.scala b/src/main/scala/units/client/L2BlockLike.scala deleted file mode 100644 index 6b30a0a4..00000000 --- a/src/main/scala/units/client/L2BlockLike.scala +++ /dev/null @@ -1,18 +0,0 @@ -package units.client - -import com.wavesplatform.common.state.ByteStr -import units.BlockHash -import units.eth.{EthAddress, EthereumConstants} -import units.util.HexBytesConverter.toByteStr - -trait L2BlockLike { - def hash: BlockHash - def parentHash: BlockHash - def height: Long - def minerRewardL2Address: EthAddress - - lazy val hashByteStr: ByteStr = toByteStr(hash) - lazy val parentHashByteStr: ByteStr = toByteStr(parentHash) - - def referencesGenesis: Boolean = height == EthereumConstants.GenesisBlockHeight + 1 -} diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala index 8c328191..214d0752 100644 --- a/src/main/scala/units/client/contract/ChainContractClient.scala +++ b/src/main/scala/units/client/contract/ChainContractClient.scala @@ -54,6 +54,10 @@ trait ChainContractClient { case BinaryDataEntry(_, v) => EthAddress.unsafeFrom(v.arr) } + def getPublicKey(rewardAddress: EthAddress): Option[PublicKey] = { + getBinaryData(s"miner_${rewardAddress}_PK").map(PublicKey(_)) + } + def getBlock(hash: BlockHash): Option[ContractBlock] = getBinaryData(s"block_$hash").orElse(getBinaryData(s"blockMeta${clean(hash)}")).map { blockMeta => val bb = ByteBuffer.wrap(blockMeta.arr) @@ -212,6 +216,15 @@ trait ChainContractClient { def getNativeTransfers(fromIndex: Long, maxItems: Long): Vector[ContractTransfer] = (fromIndex until math.min(fromIndex + maxItems, getNativeTransfersCount)).map(requireNativeTransfer).toVector + def getMinersPks: Map[EthAddress, PublicKey] = { + getAllActualMiners.flatMap { addr => + for { + rewardAddress <- getElRewardAddress(addr) + publicKey <- getPublicKey(rewardAddress) + } yield rewardAddress -> publicKey + }.toMap + } + private def getNativeTransfersCount: Long = getLongData("nativeTransfersCount").getOrElse(0L) private def requireNativeTransfer(atIndex: Long): ContractTransfer = { diff --git a/src/main/scala/units/client/contract/ContractBlock.scala b/src/main/scala/units/client/contract/ContractBlock.scala index 2beb8088..25379605 100644 --- a/src/main/scala/units/client/contract/ContractBlock.scala +++ b/src/main/scala/units/client/contract/ContractBlock.scala @@ -2,7 +2,7 @@ package units.client.contract import com.wavesplatform.common.merkle.Digest import units.BlockHash -import units.client.L2BlockLike +import units.client.CommonBlockData import units.eth.EthAddress import units.util.HexBytesConverter.toHex @@ -11,13 +11,13 @@ case class ContractBlock( parentHash: BlockHash, epoch: Int, height: Long, - minerRewardL2Address: EthAddress, + feeRecipient: EthAddress, chainId: Long, e2cTransfersRootHash: Digest, lastC2ETransferIndex: Long -) extends L2BlockLike { +) extends CommonBlockData { override def toString: String = - s"ContractBlock($hash, p=$parentHash, e=$epoch, h=$height, m=$minerRewardL2Address, c=$chainId, " + + s"ContractBlock($hash, p=$parentHash, e=$epoch, h=$height, m=$feeRecipient, c=$chainId, " + s"e2c=${if (e2cTransfersRootHash.isEmpty) "" else toHex(e2cTransfersRootHash)}, c2e=$lastC2ETransferIndex)" } diff --git a/src/main/scala/units/client/engine/EngineApiClient.scala b/src/main/scala/units/client/engine/EngineApiClient.scala index a8c0a675..04b97a96 100644 --- a/src/main/scala/units/client/engine/EngineApiClient.scala +++ b/src/main/scala/units/client/engine/EngineApiClient.scala @@ -7,9 +7,9 @@ import units.eth.EthAddress import units.{BlockHash, JobResult} trait EngineApiClient { - def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] + def forkChoiceUpdated(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] - def forkChoiceUpdateWithPayloadId( + def forkChoiceUpdatedWithPayloadId( lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, unixEpochSeconds: Long, @@ -20,19 +20,19 @@ trait EngineApiClient { def getPayload(payloadId: PayloadId): JobResult[JsObject] - def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] + def applyNewPayload(payloadJson: JsObject): JobResult[Option[BlockHash]] def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] - def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] + def getBlockByNumber(number: BlockNumber): JobResult[Option[ExecutionPayload]] - def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] + def getBlockByHash(hash: BlockHash): JobResult[Option[ExecutionPayload]] - def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] + def getLatestBlock: JobResult[ExecutionPayload] - def getLastExecutionBlock: JobResult[EcBlock] + def getBlockJsonByHash(hash: BlockHash): JobResult[Option[JsObject]] - def blockExists(hash: BlockHash): JobResult[Boolean] + def getPayloadJsonDataByHash(hash: BlockHash): JobResult[PayloadJsonData] def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] } diff --git a/src/main/scala/units/client/engine/HttpEngineApiClient.scala b/src/main/scala/units/client/engine/HttpEngineApiClient.scala index 41deab4e..03937d99 100644 --- a/src/main/scala/units/client/engine/HttpEngineApiClient.scala +++ b/src/main/scala/units/client/engine/HttpEngineApiClient.scala @@ -20,7 +20,7 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide val apiUrl: Uri = uri"${config.executionClientAddress}" - def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = { + def forkChoiceUpdated(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = { sendEngineRequest[ForkChoiceUpdatedRequest, ForkChoiceUpdatedResponse]( ForkChoiceUpdatedRequest(blockHash, finalizedBlockHash, None), BlockExecutionTimeout @@ -33,7 +33,7 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide } } - def forkChoiceUpdateWithPayloadId( + def forkChoiceUpdatedWithPayloadId( lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, unixEpochSeconds: Long, @@ -64,8 +64,8 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide sendEngineRequest[GetPayloadRequest, GetPayloadResponse](GetPayloadRequest(payloadId), NonBlockExecutionTimeout).map(_.executionPayload) } - def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = { - sendEngineRequest[NewPayloadRequest, PayloadState](NewPayloadRequest(payload), BlockExecutionTimeout).flatMap { + def applyNewPayload(payloadJson: JsObject): JobResult[Option[BlockHash]] = { + sendEngineRequest[NewPayloadRequest, PayloadState](NewPayloadRequest(payloadJson), BlockExecutionTimeout).flatMap { case PayloadState(_, _, Some(validationError)) => Left(ClientError(s"Payload validation error: $validationError")) case PayloadState(Valid, Some(latestValidHash), _) => Right(Some(latestValidHash)) case PayloadState(Syncing, latestValidHash, _) => Right(latestValidHash) @@ -79,37 +79,39 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide .map(_.value.headOption.flatMap(_.asOpt[JsObject])) } - def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = { + def getBlockByNumber(number: BlockNumber): JobResult[Option[ExecutionPayload]] = { for { - json <- getBlockByNumberJson(number.str) - blockMeta <- json.traverse(parseJson[EcBlock](_)) + json <- sendRequest[GetBlockByNumberRequest, JsObject](GetBlockByNumberRequest(number.str)) + .leftMap(err => ClientError(s"Error getting payload by number $number: $err")) + blockMeta <- json.traverse(parseJson[ExecutionPayload](_)) } yield blockMeta } - def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = { - sendRequest[GetBlockByHashRequest, EcBlock](GetBlockByHashRequest(hash)) - .leftMap(err => ClientError(s"Error getting block by hash $hash: $err")) + def getBlockByHash(hash: BlockHash): JobResult[Option[ExecutionPayload]] = { + sendRequest[GetBlockByHashRequest, ExecutionPayload](GetBlockByHashRequest(hash)) + .leftMap(err => ClientError(s"Error getting payload by hash $hash: $err")) } - def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = { + def getLatestBlock: JobResult[ExecutionPayload] = for { + lastPayloadOpt <- getBlockByNumber(BlockNumber.Latest) + lastPayload <- Either.fromOption(lastPayloadOpt, ClientError("Impossible: EC doesn't have payloads")) + } yield lastPayload + + def getBlockJsonByHash(hash: BlockHash): JobResult[Option[JsObject]] = { sendRequest[GetBlockByHashRequest, JsObject](GetBlockByHashRequest(hash)) .leftMap(err => ClientError(s"Error getting block json by hash $hash: $err")) } - def getLastExecutionBlock: JobResult[EcBlock] = for { - lastEcBlockOpt <- getBlockByNumber(BlockNumber.Latest) - lastEcBlock <- Either.fromOption(lastEcBlockOpt, ClientError("Impossible: EC doesn't have blocks")) - } yield lastEcBlock - - def blockExists(hash: BlockHash): JobResult[Boolean] = - getBlockByHash(hash).map(_.isDefined) - - private def getBlockByNumberJson(number: String): JobResult[Option[JsObject]] = { - sendRequest[GetBlockByNumberRequest, JsObject](GetBlockByNumberRequest(number)) - .leftMap(err => ClientError(s"Error getting block by number $number: $err")) + def getPayloadJsonDataByHash(hash: BlockHash): JobResult[PayloadJsonData] = { + for { + blockJsonOpt <- getBlockJsonByHash(hash) + blockJson <- Either.fromOption(blockJsonOpt, ClientError("block not found")) + payloadBodyJsonOpt <- getPayloadBodyByHash(hash) + payloadBodyJson <- Either.fromOption(payloadBodyJsonOpt, ClientError("payload body not found")) + } yield PayloadJsonData(blockJson, payloadBodyJson) } - override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = + def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = sendRequest[GetLogsRequest, List[GetLogsResponseEntry]](GetLogsRequest(hash, address, List(topic))) .leftMap(err => ClientError(s"Error getting block logs by hash $hash: $err")) .map(_.getOrElse(List.empty)) diff --git a/src/main/scala/units/client/engine/LoggedEngineApiClient.scala b/src/main/scala/units/client/engine/LoggedEngineApiClient.scala index 54a10f64..e16dc796 100644 --- a/src/main/scala/units/client/engine/LoggedEngineApiClient.scala +++ b/src/main/scala/units/client/engine/LoggedEngineApiClient.scala @@ -15,10 +15,10 @@ import scala.util.chaining.scalaUtilChainingOps class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient { protected val log: LoggerFacade = LoggerFacade(LoggerFactory.getLogger(underlying.getClass)) - override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = - wrap(s"forkChoiceUpdate($blockHash, f=$finalizedBlockHash)", underlying.forkChoiceUpdate(blockHash, finalizedBlockHash)) + override def forkChoiceUpdated(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = + wrap(s"forkChoiceUpdated($blockHash, f=$finalizedBlockHash)", underlying.forkChoiceUpdated(blockHash, finalizedBlockHash)) - override def forkChoiceUpdateWithPayloadId( + override def forkChoiceUpdatedWithPayloadId( lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, unixEpochSeconds: Long, @@ -26,34 +26,39 @@ class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient prevRandao: String, withdrawals: Vector[Withdrawal] ): JobResult[PayloadId] = wrap( - s"forkChoiceUpdateWithPayloadId(l=$lastBlockHash, f=$finalizedBlockHash, ts=$unixEpochSeconds, m=$suggestedFeeRecipient, " + + s"forkChoiceUpdatedWithPayloadId(l=$lastBlockHash, f=$finalizedBlockHash, ts=$unixEpochSeconds, m=$suggestedFeeRecipient, " + s"r=$prevRandao, w={${withdrawals.mkString(", ")}}", - underlying.forkChoiceUpdateWithPayloadId(lastBlockHash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals) + underlying.forkChoiceUpdatedWithPayloadId(lastBlockHash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals) ) override def getPayload(payloadId: PayloadId): JobResult[JsObject] = - wrap(s"getPayload($payloadId)", underlying.getPayload(payloadId), filteredJson) + wrap(s"getPayload($payloadId)", underlying.getPayload(payloadId), filteredJsonStr) - override def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = - wrap(s"applyNewPayload(${filteredJson(payload)})", underlying.applyNewPayload(payload), _.fold("None")(_.toString)) + override def applyNewPayload(payloadJson: JsObject): JobResult[Option[BlockHash]] = + wrap(s"applyNewPayload(${filteredJsonStr(payloadJson)})", underlying.applyNewPayload(payloadJson), _.fold("None")(identity)) override def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] = - wrap(s"getPayloadBodyByHash($hash)", underlying.getPayloadBodyByHash(hash), _.fold("None")(filteredJson)) + wrap(s"getPayloadBodyByHash($hash)", underlying.getPayloadBodyByHash(hash), _.fold("None")(filteredJsonStr)) - override def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = + override def getBlockByNumber(number: BlockNumber): JobResult[Option[ExecutionPayload]] = wrap(s"getBlockByNumber($number)", underlying.getBlockByNumber(number), _.fold("None")(_.toString)) - override def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = + override def getBlockByHash(hash: BlockHash): JobResult[Option[ExecutionPayload]] = wrap(s"getBlockByHash($hash)", underlying.getBlockByHash(hash), _.fold("None")(_.toString)) - override def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = - wrap(s"getBlockByHashJson($hash)", underlying.getBlockByHashJson(hash), _.fold("None")(filteredJson)) + override def getLatestBlock: JobResult[ExecutionPayload] = + wrap("getLatestBlock", underlying.getLatestBlock) - override def getLastExecutionBlock: JobResult[EcBlock] = - wrap("getLastExecutionBlock", underlying.getLastExecutionBlock) + override def getBlockJsonByHash(hash: BlockHash): JobResult[Option[JsObject]] = + wrap(s"getBlockJsonByHash($hash)", underlying.getBlockJsonByHash(hash), _.fold("None")(filteredJsonStr)) - override def blockExists(hash: BlockHash): JobResult[Boolean] = - wrap(s"blockExists($hash)", underlying.blockExists(hash)) + override def getPayloadJsonDataByHash(hash: BlockHash): JobResult[PayloadJsonData] = { + wrap( + s"getPayloadJsonDataByHash($hash)", + underlying.getPayloadJsonDataByHash(hash), + pjd => PayloadJsonData(filteredJson(pjd.blockJson), filteredJson(pjd.bodyJson)).toString + ) + } override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = wrap(s"getLogs($hash, a=$address, t=$topic)", underlying.getLogs(hash, address, topic), _.view.map(_.data).mkString("{", ", ", "}")) @@ -68,9 +73,12 @@ class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient } } - private def filteredJson(jsObject: JsObject): String = JsObject( + private def filteredJson(jsObject: JsObject): JsObject = JsObject( jsObject.fields.filterNot { case (k, _) => excludedJsonFields.contains(k) } - ).toString() + ) + + private def filteredJsonStr(jsObject: JsObject): String = + filteredJson(jsObject).toString } object LoggedEngineApiClient { diff --git a/src/main/scala/units/client/engine/model/EcBlock.scala b/src/main/scala/units/client/engine/model/ExecutionPayload.scala similarity index 77% rename from src/main/scala/units/client/engine/model/EcBlock.scala rename to src/main/scala/units/client/engine/model/ExecutionPayload.scala index 2d8cbb54..b5916a39 100644 --- a/src/main/scala/units/client/engine/model/EcBlock.scala +++ b/src/main/scala/units/client/engine/model/ExecutionPayload.scala @@ -5,34 +5,33 @@ import play.api.libs.functional.syntax.* import play.api.libs.json.* import play.api.libs.json.Format.GenericFormat import units.BlockHash -import units.client.L2BlockLike +import units.client.CommonBlockData import units.eth.EthAddress import units.util.HexBytesConverter.* -/** Block in EC API, not a payload of Engine API! See BlockHeader in besu. - * @param timestamp +/** @param timestamp * In seconds, see ProcessableBlockHeader.timestamp comment <- SealableBlockHeader <- BlockHeader * https://besu.hyperledger.org/stable/public-networks/reference/engine-api/objects#execution-payload-object tells about milliseconds */ -case class EcBlock( +case class ExecutionPayload( hash: BlockHash, parentHash: BlockHash, stateRoot: String, height: Long, timestamp: Long, - minerRewardL2Address: EthAddress, + feeRecipient: EthAddress, baseFeePerGas: Uint256, gasLimit: Long, gasUsed: Long, prevRandao: String, withdrawals: Vector[Withdrawal] -) extends L2BlockLike { +) extends CommonBlockData { override def toString: String = - s"EcBlock($hash, p=$parentHash, h=$height, t=$timestamp, m=$minerRewardL2Address, w={${withdrawals.mkString(", ")}})" + s"ExecutionPayload($hash, p=$parentHash, h=$height, t=$timestamp, m=$feeRecipient, w={${withdrawals.mkString(", ")}})" } -object EcBlock { - implicit val reads: Reads[EcBlock] = ( +object ExecutionPayload { + implicit val reads: Reads[ExecutionPayload] = ( (JsPath \ "hash").read[BlockHash] and (JsPath \ "parentHash").read[BlockHash] and (JsPath \ "stateRoot").read[String] and @@ -44,5 +43,5 @@ object EcBlock { (JsPath \ "gasUsed").read[String].map(toLong) and (JsPath \ "mixHash").read[String] and (JsPath \ "withdrawals").readWithDefault(Vector.empty[Withdrawal]) - )(EcBlock.apply _) + )(ExecutionPayload.apply _) } diff --git a/src/main/scala/units/util/BlockToPayloadMapper.scala b/src/main/scala/units/client/engine/model/PayloadJsonData.scala similarity index 77% rename from src/main/scala/units/util/BlockToPayloadMapper.scala rename to src/main/scala/units/client/engine/model/PayloadJsonData.scala index fbbc1f20..bd8b0be6 100644 --- a/src/main/scala/units/util/BlockToPayloadMapper.scala +++ b/src/main/scala/units/client/engine/model/PayloadJsonData.scala @@ -1,9 +1,25 @@ -package units.util +package units.client.engine.model -import units.eth.EthereumConstants import play.api.libs.json.{JsObject, JsString} +import units.client.engine.model.PayloadJsonData.fieldsMapping +import units.eth.EthereumConstants + +case class PayloadJsonData(blockJson: JsObject, bodyJson: JsObject) { + def toPayloadJson: JsObject = { + val blockJsonData = blockJson.value + + JsObject( + fieldsMapping.flatMap { case (blockField, payloadField) => + blockJsonData.get(blockField).map(payloadField -> _) + } ++ List( + "blobGasUsed" -> JsString(EthereumConstants.ZeroHex), + "excessBlobGas" -> JsString(EthereumConstants.ZeroHex) + ) + ) ++ bodyJson + } +} -object BlockToPayloadMapper { +object PayloadJsonData { private val commonFields = Seq( "parentHash", @@ -21,18 +37,4 @@ object BlockToPayloadMapper { Seq("miner" -> "feeRecipient", "number" -> "blockNumber", "hash" -> "blockHash", "mixHash" -> "prevRandao") ++ commonFields.map(field => field -> field ) - - def toPayloadJson(blockJson: JsObject, payloadBodyJson: JsObject): JsObject = { - val blockJsonData = blockJson.value - - JsObject( - fieldsMapping.flatMap { case (blockField, payloadField) => - blockJsonData.get(blockField).map(payloadField -> _) - } ++ List( - "blobGasUsed" -> JsString(EthereumConstants.ZeroHex), - "excessBlobGas" -> JsString(EthereumConstants.ZeroHex) - ) - ) ++ payloadBodyJson - } - -} +} \ No newline at end of file diff --git a/src/main/scala/units/eth/EmptyL2Block.scala b/src/main/scala/units/eth/EmptyPayload.scala similarity index 85% rename from src/main/scala/units/eth/EmptyL2Block.scala rename to src/main/scala/units/eth/EmptyPayload.scala index f0165c66..f6909dc7 100644 --- a/src/main/scala/units/eth/EmptyL2Block.scala +++ b/src/main/scala/units/eth/EmptyPayload.scala @@ -4,10 +4,10 @@ import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.rlp.{RlpEncoder, RlpList, RlpString} import play.api.libs.json.{JsObject, Json} import units.BlockHash -import units.client.engine.model.EcBlock +import units.client.engine.model.ExecutionPayload import units.util.HexBytesConverter -object EmptyL2Block { +object EmptyPayload { case class Params( parentHash: BlockHash, parentStateRoot: String, @@ -20,24 +20,24 @@ object EmptyL2Block { private val InternalBlockTimestampDiff = 1 // seconds - def mkExecutionPayload(parent: EcBlock, feeRecipient: EthAddress = EthAddress.empty): JsObject = - mkExecutionPayload( + def mkExecutionPayloadJson(parentPayload: ExecutionPayload, feeRecipient: EthAddress = EthAddress.empty): JsObject = + mkExecutionPayloadJson( Params( - parentHash = parent.hash, - parentStateRoot = parent.stateRoot, - parentGasLimit = parent.gasLimit, - newBlockTimestamp = parent.timestamp + InternalBlockTimestampDiff, - newBlockNumber = parent.height + 1, + parentHash = parentPayload.hash, + parentStateRoot = parentPayload.stateRoot, + parentGasLimit = parentPayload.gasLimit, + newBlockTimestamp = parentPayload.timestamp + InternalBlockTimestampDiff, + newBlockNumber = parentPayload.height + 1, baseFee = calculateGasFee( - parentGasLimit = parent.gasLimit, - parentBaseFeePerGas = parent.baseFeePerGas, - parentGasUsed = parent.gasUsed + parentGasLimit = parentPayload.gasLimit, + parentBaseFeePerGas = parentPayload.baseFeePerGas, + parentGasUsed = parentPayload.gasUsed ), feeRecipient = feeRecipient ) ) - def mkExecutionPayload(params: Params): JsObject = Json.obj( + def mkExecutionPayloadJson(params: Params): JsObject = Json.obj( "parentHash" -> params.parentHash, "feeRecipient" -> params.feeRecipient, "stateRoot" -> params.parentStateRoot, diff --git a/src/main/scala/units/network/BasicMessagesRepo.scala b/src/main/scala/units/network/BasicMessagesRepo.scala index 39d2dc8a..981afd66 100644 --- a/src/main/scala/units/network/BasicMessagesRepo.scala +++ b/src/main/scala/units/network/BasicMessagesRepo.scala @@ -1,12 +1,9 @@ package units.network import cats.syntax.either.* -import com.google.common.primitives.Bytes -import com.wavesplatform.common.state.ByteStr -import com.wavesplatform.crypto import com.wavesplatform.crypto.SignatureLength import units.util.HexBytesConverter.* -import units.{BlockHash, NetworkL2Block} +import units.BlockHash import com.wavesplatform.network.message.Message.MessageCode import com.wavesplatform.network.message.{Message, MessageSpec} import com.wavesplatform.network.{InetSocketAddressSeqSpec, NetworkServer} @@ -36,42 +33,28 @@ object PeersSpec extends InetSocketAddressSeqSpec[KnownPeers] { override protected def wrap(addresses: Seq[InetSocketAddress]): KnownPeers = KnownPeers(addresses) } -object GetBlockL2Spec extends MessageSpec[GetBlock] { +object GetPayloadSpec extends MessageSpec[GetPayload] { override val messageCode: MessageCode = 3: Byte override val maxLength: Int = SignatureLength - override def serializeData(msg: GetBlock): Array[Byte] = toBytes(msg.hash) + override def serializeData(msg: GetPayload): Array[Byte] = + toBytes(msg.hash) - override def deserializeData(bytes: Array[Byte]): Try[GetBlock] = Try { - require( - NetworkL2Block.validateReferenceLength(bytes.length), - s"Invalid hash length ${bytes.length} in GetBlock message, expecting ${crypto.DigestLength}" - ) - GetBlock(BlockHash(bytes)) - } + override def deserializeData(bytes: Array[Byte]): Try[GetPayload] = + Try(GetPayload(BlockHash(bytes))) } -object BlockSpec extends MessageSpec[NetworkL2Block] { +object PayloadSpec extends MessageSpec[PayloadMessage] { override val messageCode: MessageCode = 4: Byte override val maxLength: Int = NetworkServer.MaxFrameLength - override def serializeData(block: NetworkL2Block): Array[Byte] = { - val signatureBytes = block.signature.map(sig => Bytes.concat(Array(1.toByte), sig.arr)).getOrElse(Array(0.toByte)) - Bytes.concat(signatureBytes, block.payloadBytes) - } - - override def deserializeData(bytes: Array[Byte]): Try[NetworkL2Block] = { - // We need a signature only for blocks those are not confirmed on the chain contract - val isWithSignature = bytes.headOption.contains(1.toByte) - val signature = if (isWithSignature) Some(ByteStr(bytes.slice(1, SignatureLength + 1))) else None - val payloadOffset = if (isWithSignature) SignatureLength + 1 else 1 - for { - _ <- Either.cond(signature.forall(_.size == SignatureLength), (), new RuntimeException("Invalid block signature size")).toTry - block <- NetworkL2Block(bytes.drop(payloadOffset), signature).leftMap(err => new RuntimeException(err.message)).toTry - } yield block - } + override def serializeData(payloadMsg: PayloadMessage): Array[Byte] = + payloadMsg.toBytes + + override def deserializeData(bytes: Array[Byte]): Try[PayloadMessage] = + PayloadMessage.fromBytes(bytes).leftMap(err => new IllegalArgumentException(err)).toTry } object BasicMessagesRepo { @@ -80,8 +63,8 @@ object BasicMessagesRepo { val specs: Seq[Spec] = Seq( GetPeersSpec, PeersSpec, - GetBlockL2Spec, - BlockSpec + GetPayloadSpec, + PayloadSpec ) val specsByCodes: Map[Byte, Spec] = specs.map(s => s.messageCode -> s).toMap diff --git a/src/main/scala/units/network/BlocksObserver.scala b/src/main/scala/units/network/BlocksObserver.scala deleted file mode 100644 index 33d8844a..00000000 --- a/src/main/scala/units/network/BlocksObserver.scala +++ /dev/null @@ -1,15 +0,0 @@ -package units.network - -import units.network.BlocksObserverImpl.BlockWithChannel -import com.wavesplatform.network.ChannelObservable -import monix.eval.Task -import monix.execution.CancelableFuture -import units.{BlockHash, NetworkL2Block} - -trait BlocksObserver { - def getBlockStream: ChannelObservable[NetworkL2Block] - - def requestBlock(req: BlockHash): Task[BlockWithChannel] - - def loadBlock(req: BlockHash): CancelableFuture[BlockWithChannel] -} diff --git a/src/main/scala/units/network/BlocksObserverImpl.scala b/src/main/scala/units/network/BlocksObserverImpl.scala deleted file mode 100644 index 2fb53f52..00000000 --- a/src/main/scala/units/network/BlocksObserverImpl.scala +++ /dev/null @@ -1,126 +0,0 @@ -package units.network - -import com.google.common.cache.CacheBuilder -import com.wavesplatform.network.ChannelObservable -import com.wavesplatform.utils.ScorexLogging -import io.netty.channel.Channel -import io.netty.channel.group.DefaultChannelGroup -import monix.eval.Task -import monix.execution.{Cancelable, CancelableFuture, CancelablePromise, Scheduler} -import monix.reactive.subjects.ConcurrentSubject -import units.network.BlocksObserverImpl.{BlockWithChannel, State} -import units.{BlockHash, NetworkL2Block} - -import java.time.Duration -import scala.concurrent.duration.FiniteDuration -import scala.jdk.CollectionConverters.* -import scala.util.{Failure, Success} - -class BlocksObserverImpl(allChannels: DefaultChannelGroup, blocks: ChannelObservable[NetworkL2Block], syncTimeout: FiniteDuration)(implicit - sc: Scheduler -) extends BlocksObserver - with ScorexLogging { - - private var state: State = State.Idle(None) - private val blocksResult: ConcurrentSubject[BlockWithChannel, BlockWithChannel] = - ConcurrentSubject.publish[BlockWithChannel] - - def loadBlock(req: BlockHash): CancelableFuture[BlockWithChannel] = knownBlockCache.getIfPresent(req) match { - case null => - val p = CancelablePromise[BlockWithChannel]() - sc.execute { () => - val candidate = state match { - case State.LoadingBlock(_, nextAttempt, promise) => - nextAttempt.cancel() - promise.complete(Failure(new NoSuchElementException("Loading was canceled"))) - None - case State.Idle(candidate) => candidate - } - state = State.LoadingBlock(req, requestFromNextChannel(req, candidate, Set.empty).runToFuture, p) - } - p.future - case (ch, block) => - CancelablePromise.successful(ch -> block).future - } - - private val knownBlockCache = CacheBuilder - .newBuilder() - .expireAfterWrite(Duration.ofMinutes(10)) - .maximumSize(100) - .build[BlockHash, BlockWithChannel]() - - blocks - .foreach { case v@(ch, block) => - state = state match { - case State.LoadingBlock(expectedHash, nextAttempt, p) if expectedHash == block.hash => - nextAttempt.cancel() - p.complete(Success(ch -> block)) - State.Idle(Some(ch)) - case other => other - } - - knownBlockCache.put(block.hash, v) - blocksResult.onNext(v) - } - - def getBlockStream: ChannelObservable[NetworkL2Block] = blocksResult - - def requestBlock(req: BlockHash): Task[BlockWithChannel] = Task - .defer { - log.info(s"Loading block $req") - knownBlockCache.getIfPresent(req) match { - case null => - val p = CancelablePromise[BlockWithChannel]() - - val candidate = state match { - case l: State.LoadingBlock => - log.trace(s"No longer waiting for block ${l.blockHash}, will load $req instead") - l.nextAttempt.cancel() - l.promise.future.cancel() - None - case State.Idle(candidate) => - candidate - } - - state = State.LoadingBlock( - req, - requestFromNextChannel(req, candidate, Set.empty).runToFuture, - p - ) - - Task.fromCancelablePromise(p) - case (ch, block) => - Task.pure(ch -> block) - } - } - .executeOn(sc) - - private def requestFromNextChannel(req: BlockHash, candidate: Option[Channel], excluded: Set[Channel]): Task[Unit] = Task { - candidate.filterNot(excluded).orElse(nextOpenChannel(excluded)) match { - case None => - log.trace(s"No channel to request $req") - Set.empty[Channel] - case Some(ch) => - ch.writeAndFlush(GetBlock(req)) - excluded ++ Set(ch) - } - }.flatMap(newExcluded => requestFromNextChannel(req, candidate, newExcluded).delayExecution(syncTimeout)) - - private def nextOpenChannel(exclude: Set[Channel]): Option[Channel] = - allChannels.asScala.find(c => !exclude(c) && c.isOpen) - -} - -object BlocksObserverImpl { - - type BlockWithChannel = (Channel, NetworkL2Block) - - sealed trait State - - object State { - case class LoadingBlock(blockHash: BlockHash, nextAttempt: Cancelable, promise: CancelablePromise[BlockWithChannel]) extends State - - case class Idle(pinnedChannel: Option[Channel]) extends State - } - -} diff --git a/src/main/scala/units/network/HistoryReplier.scala b/src/main/scala/units/network/HistoryReplier.scala index 04db98ba..b7083d46 100644 --- a/src/main/scala/units/network/HistoryReplier.scala +++ b/src/main/scala/units/network/HistoryReplier.scala @@ -7,8 +7,7 @@ import io.netty.channel.ChannelHandler.Sharable import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter} import monix.execution.Scheduler import units.client.engine.EngineApiClient -import units.util.BlockToPayloadMapper -import units.{BlockHash, ClientError, NetworkL2Block} +import units.{BlockHash, ClientError} import scala.concurrent.Future import scala.util.{Failure, Success} @@ -27,27 +26,22 @@ class HistoryReplier(engineApiClient: EngineApiClient)(implicit sc: Scheduler) e } override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = msg match { - case GetBlock(hash) => + case GetPayload(hash) => respondWith( ctx, - loadBlockL2(hash) + loadPayload(hash) .map { - case Right(blockL2) => - RawBytes(BlockSpec.messageCode, BlockSpec.serializeData(blockL2)) - case Left(err) => throw new NoSuchElementException(s"Error loading block $hash: $err") + case Right(payloadMsg) => + RawBytes(PayloadSpec.messageCode, PayloadSpec.serializeData(payloadMsg)) + case Left(err) => throw new NoSuchElementException(s"Error loading block $hash payload: $err") } ) case _ => super.channelRead(ctx, msg) } - private def loadBlockL2(hash: BlockHash): Future[Either[ClientError, NetworkL2Block]] = Future { - for { - blockJsonOpt <- engineApiClient.getBlockByHashJson(hash) - blockJson <- Either.fromOption(blockJsonOpt, ClientError("block not found")) - payloadBodyJsonOpt <- engineApiClient.getPayloadBodyByHash(hash) - payloadBodyJson <- Either.fromOption(payloadBodyJsonOpt, ClientError("payload body not found")) - payload = BlockToPayloadMapper.toPayloadJson(blockJson, payloadBodyJson) - blockL2 <- NetworkL2Block(payload) - } yield blockL2 + private def loadPayload(hash: BlockHash): Future[Either[ClientError, PayloadMessage]] = Future { + engineApiClient.getPayloadJsonDataByHash(hash).flatMap { payloadJsonData => + PayloadMessage(payloadJsonData.toPayloadJson).leftMap(ClientError.apply) + } } } diff --git a/src/main/scala/units/network/LegacyFrameCodec.scala b/src/main/scala/units/network/LegacyFrameCodec.scala index e8c34893..0d100d13 100644 --- a/src/main/scala/units/network/LegacyFrameCodec.scala +++ b/src/main/scala/units/network/LegacyFrameCodec.scala @@ -1,6 +1,5 @@ package units.network -import units.NetworkL2Block import com.wavesplatform.network.BasicMessagesRepo.Spec import com.wavesplatform.network.LegacyFrameCodec.MessageRawData import com.wavesplatform.network.message.Message.MessageCode @@ -12,12 +11,12 @@ class LegacyFrameCodec(peerDatabase: PeerDatabase) extends LFC(peerDatabase) { override protected def specsByCodes: Map[MessageCode, Spec] = BasicMessagesRepo.specsByCodes override protected def messageToRawData(msg: Any): MessageRawData = { - val rawBytesL2 = (msg: @unchecked) match { - case rb: RawBytes => rb - case block: NetworkL2Block => RawBytes.from(BlockSpec, block) + val rawBytes = (msg: @unchecked) match { + case rb: RawBytes => rb + case payloadMsg: PayloadMessage => RawBytes.from(PayloadSpec, payloadMsg) } - MessageRawData(rawBytesL2.code, rawBytesL2.data) + MessageRawData(rawBytes.code, rawBytes.data) } protected def rawDataToMessage(rawData: MessageRawData): AnyRef = diff --git a/src/main/scala/units/network/Message.scala b/src/main/scala/units/network/Message.scala index 0daf8bf6..c0f452d4 100644 --- a/src/main/scala/units/network/Message.scala +++ b/src/main/scala/units/network/Message.scala @@ -12,7 +12,7 @@ case object GetPeers extends Message case class KnownPeers(peers: Seq[InetSocketAddress]) extends Message -case class GetBlock(hash: BlockHash) extends Message +case class GetPayload(hash: BlockHash) extends Message case class RawBytes(code: Byte, data: Array[Byte]) extends Message { override def toString: String = s"RawBytes($code, ${data.length} bytes)" diff --git a/src/main/scala/units/network/MessageCodec.scala b/src/main/scala/units/network/MessageCodec.scala index 0ec96c63..2194f649 100644 --- a/src/main/scala/units/network/MessageCodec.scala +++ b/src/main/scala/units/network/MessageCodec.scala @@ -21,7 +21,7 @@ class MessageCodec(peerDatabase: PeerDatabase) extends MessageToMessageCodec[Raw // With a spec case GetPeers => RawBytes.from(GetPeersSpec, GetPeers) case k: KnownPeers => RawBytes.from(PeersSpec, k) - case g: GetBlock => RawBytes.from(GetBlockL2Spec, g) + case g: GetPayload => RawBytes.from(GetPayloadSpec, g) case _ => throw new IllegalArgumentException(s"Can't send message $msg to $ctx (unsupported)") diff --git a/src/main/scala/units/network/MessageObserver.scala b/src/main/scala/units/network/MessageObserver.scala index 484b1396..9b055f91 100644 --- a/src/main/scala/units/network/MessageObserver.scala +++ b/src/main/scala/units/network/MessageObserver.scala @@ -2,24 +2,23 @@ package units.network import com.wavesplatform.utils.Schedulers import io.netty.channel.ChannelHandler.Sharable -import io.netty.channel.{Channel, ChannelHandlerContext, ChannelInboundHandlerAdapter} +import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter} import monix.execution.schedulers.SchedulerService import monix.reactive.subjects.{ConcurrentSubject, Subject} -import units.NetworkL2Block @Sharable class MessageObserver extends ChannelInboundHandlerAdapter { private implicit val scheduler: SchedulerService = Schedulers.fixedPool(2, "message-observer-l2") - val blocks: Subject[(Channel, NetworkL2Block), (Channel, NetworkL2Block)] = ConcurrentSubject.publish[(Channel, NetworkL2Block)] + val payloads: Subject[PayloadMessageWithChannel, PayloadMessageWithChannel] = ConcurrentSubject.publish[PayloadMessageWithChannel] override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = msg match { - case b: NetworkL2Block => blocks.onNext((ctx.channel(), b)) - case _ => super.channelRead(ctx, msg) + case pm: PayloadMessage => payloads.onNext(PayloadMessageWithChannel(pm, ctx.channel())) + case _ => super.channelRead(ctx, msg) } def shutdown(): Unit = { - blocks.onComplete() + payloads.onComplete() } } diff --git a/src/main/scala/units/network/PayloadMessage.scala b/src/main/scala/units/network/PayloadMessage.scala new file mode 100644 index 00000000..59705e69 --- /dev/null +++ b/src/main/scala/units/network/PayloadMessage.scala @@ -0,0 +1,103 @@ +package units.network + +import cats.syntax.either.* +import com.google.common.primitives.Bytes +import com.wavesplatform.account.{PrivateKey, PublicKey} +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.crypto +import com.wavesplatform.crypto.SignatureLength +import play.api.libs.json.{JsObject, Json} +import units.client.engine.model.{ExecutionPayload, Withdrawal} +import units.eth.EthAddress +import units.{BlockHash, ExecutionPayloadInfo} +import units.util.HexBytesConverter.* + +import scala.util.Try + +class PayloadMessage private ( + payloadJson: JsObject, + val hash: BlockHash, + val feeRecipient: EthAddress, + val signature: Option[ByteStr] +) { + lazy val jsonBytes: Array[Byte] = Json.toBytes(payloadJson) + + lazy val payloadInfo: Either[String, ExecutionPayloadInfo] = { + (for { + timestamp <- (payloadJson \ "timestamp").asOpt[String].map(toLong).toRight("timestamp not defined") + height <- (payloadJson \ "blockNumber").asOpt[String].map(toLong).toRight("height not defined") + parentHash <- (payloadJson \ "parentHash").asOpt[BlockHash].toRight("parent hash not defined") + stateRoot <- (payloadJson \ "stateRoot").asOpt[String].toRight("state root not defined") + feeRecipient <- (payloadJson \ "feeRecipient").asOpt[EthAddress].toRight("fee recipient not defined") + baseFeePerGas <- (payloadJson \ "baseFeePerGas").asOpt[String].map(toUint256).toRight("baseFeePerGas not defined") + gasLimit <- (payloadJson \ "gasLimit").asOpt[String].map(toLong).toRight("gasLimit not defined") + gasUsed <- (payloadJson \ "gasUsed").asOpt[String].map(toLong).toRight("gasUsed not defined") + prevRandao <- (payloadJson \ "prevRandao").asOpt[String].toRight("prevRandao not defined") + withdrawals <- (payloadJson \ "withdrawals").asOpt[Vector[Withdrawal]].toRight("withdrawals are not defined") + } yield { + ExecutionPayloadInfo( + ExecutionPayload( + hash, + parentHash, + stateRoot, + height, + timestamp, + feeRecipient, + baseFeePerGas, + gasLimit, + gasUsed, + prevRandao, + withdrawals + ), + payloadJson + ) + }).leftMap(err => s"Error creating payload info for block $hash: $err") + } + + def toBytes: Array[Byte] = { + val signatureBytes = signature.map(sig => Bytes.concat(Array(1.toByte), sig.arr)).getOrElse(Array(0.toByte)) + Bytes.concat(signatureBytes, jsonBytes) + } + + def isSignatureValid(pk: PublicKey): Boolean = + signature.exists(crypto.verify(_, jsonBytes, pk, checkWeakPk = true)) +} + +object PayloadMessage { + def apply(payloadJson: JsObject): Either[String, PayloadMessage] = + apply(payloadJson, None) + + def apply(payloadJson: JsObject, hash: BlockHash, feeRecipient: EthAddress, signature: Option[ByteStr]): PayloadMessage = + new PayloadMessage(payloadJson, hash, feeRecipient, signature) + + def apply(payloadJson: JsObject, signature: Option[ByteStr]): Either[String, PayloadMessage] = + (for { + hash <- (payloadJson \ "blockHash").asOpt[BlockHash].toRight("block hash not defined") + feeRecipient <- (payloadJson \ "feeRecipient").asOpt[EthAddress].toRight("fee recipient not defined") + } yield PayloadMessage(payloadJson, hash, feeRecipient, signature)) + .leftMap(err => s"Error creating payload message: $err") + + def apply(payloadBytes: Array[Byte], signature: Option[ByteStr]): Either[String, PayloadMessage] = for { + payload <- Try(Json.parse(payloadBytes).as[JsObject]).toEither.leftMap(err => s"Payload bytes are not a valid JSON object: ${err.getMessage}") + payloadMsg <- apply(payload, signature) + } yield payloadMsg + + def signed(payloadJson: JsObject, signer: PrivateKey): Either[String, PayloadMessage] = { + val signature = crypto.sign(signer, Json.toBytes(payloadJson)) + PayloadMessage(payloadJson, Some(signature)) + } + + def fromBytes(bytes: Array[Byte]): Either[String, PayloadMessage] = { + val isWithSignature = bytes.headOption.contains(1.toByte) + val signature = if (isWithSignature) Some(ByteStr(bytes.slice(1, SignatureLength + 1))) else None + val payloadOffset = if (isWithSignature) SignatureLength + 1 else 1 + + for { + _ <- validateSignatureLength(signature) + pm <- apply(bytes.drop(payloadOffset), signature) + } yield pm + } + + private def validateSignatureLength(signature: Option[ByteStr]): Either[String, Unit] = + Either.cond(signature.forall(_.size == SignatureLength), (), "Invalid payload signature size") +} diff --git a/src/main/scala/units/network/PayloadMessageWithChannel.scala b/src/main/scala/units/network/PayloadMessageWithChannel.scala new file mode 100644 index 00000000..c96f7e05 --- /dev/null +++ b/src/main/scala/units/network/PayloadMessageWithChannel.scala @@ -0,0 +1,5 @@ +package units.network + +import io.netty.channel.Channel + +case class PayloadMessageWithChannel(pm: PayloadMessage, ch: Channel) diff --git a/src/main/scala/units/network/PayloadObserver.scala b/src/main/scala/units/network/PayloadObserver.scala new file mode 100644 index 00000000..e838153c --- /dev/null +++ b/src/main/scala/units/network/PayloadObserver.scala @@ -0,0 +1,20 @@ +package units.network + +import com.wavesplatform.account.{PrivateKey, PublicKey} +import monix.execution.CancelableFuture +import monix.reactive.Observable +import play.api.libs.json.JsObject +import units.eth.EthAddress +import units.{BlockHash, ExecutionPayloadInfo} + +trait PayloadObserver { + def getPayloadStream: Observable[ExecutionPayloadInfo] + + def loadPayload(req: BlockHash): CancelableFuture[ExecutionPayloadInfo] + + def broadcastSigned(payloadJson: JsObject, signer: PrivateKey): Either[String, PayloadMessage] + + def broadcast(hash: BlockHash): Unit + + def updateMinerPublicKeys(newKeys: Map[EthAddress, PublicKey]): Unit +} diff --git a/src/main/scala/units/network/PayloadObserverImpl.scala b/src/main/scala/units/network/PayloadObserverImpl.scala new file mode 100644 index 00000000..c95125d1 --- /dev/null +++ b/src/main/scala/units/network/PayloadObserverImpl.scala @@ -0,0 +1,157 @@ +package units.network + +import cats.syntax.either.* +import com.google.common.cache.CacheBuilder +import com.wavesplatform.account.{PrivateKey, PublicKey} +import com.wavesplatform.network.ChannelGroupExt +import com.wavesplatform.utils.ScorexLogging +import io.netty.channel.Channel +import io.netty.channel.group.DefaultChannelGroup +import monix.eval.Task +import monix.execution.{Cancelable, CancelableFuture, CancelablePromise, Scheduler} +import monix.reactive.Observable +import monix.reactive.subjects.ConcurrentSubject +import play.api.libs.json.JsObject +import units.eth.EthAddress +import units.network.PayloadObserverImpl.State +import units.{BlockHash, ExecutionPayloadInfo} + +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import scala.concurrent.duration.FiniteDuration +import scala.jdk.CollectionConverters.* +import scala.util.{Failure, Success, Try} + +class PayloadObserverImpl( + allChannels: DefaultChannelGroup, + payloads: Observable[PayloadMessageWithChannel], + initialMinersKeys: Map[EthAddress, PublicKey], + syncTimeout: FiniteDuration +)(implicit sc: Scheduler) + extends PayloadObserver + with ScorexLogging { + + private var state: State = State.Idle(None) + private val lastPayloadMessages: ConcurrentHashMap[BlockHash, PayloadMessageWithChannel] = + new ConcurrentHashMap[BlockHash, PayloadMessageWithChannel]() + + private val payloadsResult: ConcurrentSubject[ExecutionPayloadInfo, ExecutionPayloadInfo] = + ConcurrentSubject.publish[ExecutionPayloadInfo] + private val addrToPK: ConcurrentHashMap[EthAddress, PublicKey] = new ConcurrentHashMap[EthAddress, PublicKey](initialMinersKeys.asJava) + + private val knownPayloadCache = CacheBuilder + .newBuilder() + .expireAfterWrite(Duration.ofMinutes(10)) + .maximumSize(100) + .build[BlockHash, ExecutionPayloadInfo]() + + payloads + .foreach { case v @ PayloadMessageWithChannel(pm, ch) => + state = state match { + case State.LoadingPayload(expectedHash, nextAttempt, p) if expectedHash == pm.hash => + pm.payloadInfo match { + case Right(epi) => + nextAttempt.cancel() + p.complete(Success(epi)) + lastPayloadMessages.put(pm.hash, v) + knownPayloadCache.put(pm.hash, epi) + payloadsResult.onNext(epi) + case Left(err) => log.debug(err) + } + State.Idle(Some(ch)) + case other => + Option(addrToPK.get(pm.feeRecipient)) match { + case Some(pk) => + if (pm.isSignatureValid(pk)) { + pm.payloadInfo match { + case Right(epi) => + lastPayloadMessages.put(pm.hash, v) + knownPayloadCache.put(pm.hash, epi) + payloadsResult.onNext(epi) + case Left(err) => log.debug(err) + } + } else { + log.debug(s"Invalid signature for payload ${pm.hash}") + } + case None => + log.debug(s"Payload ${pm.hash} fee recipient ${pm.feeRecipient} is unknown miner") + } + other + } + } + + def loadPayload(req: BlockHash): CancelableFuture[ExecutionPayloadInfo] = { + log.info(s"Loading payload $req") + Option(knownPayloadCache.getIfPresent(req)) match { + case None => + val p = CancelablePromise[ExecutionPayloadInfo]() + sc.execute { () => + val candidate = state match { + case State.LoadingPayload(_, nextAttempt, promise) => + nextAttempt.cancel() + promise.complete(Failure(new NoSuchElementException("Loading was canceled"))) + None + case State.Idle(candidate) => candidate + } + state = State.LoadingPayload(req, requestFromNextChannel(req, candidate, Set.empty).runToFuture, p) + } + p.future + case Some(epi) => + CancelablePromise.successful(epi).future + } + } + + def getPayloadStream: Observable[ExecutionPayloadInfo] = payloadsResult + + def broadcastSigned(payloadJson: JsObject, signer: PrivateKey): Either[String, PayloadMessage] = for { + pm <- PayloadMessage.signed(payloadJson, signer) + _ = log.debug(s"Broadcasting block ${pm.hash} payload") + _ <- Try(allChannels.broadcast(pm)).toEither.leftMap(err => s"Failed to broadcast block ${pm.hash} payload: ${err.toString}") + } yield pm + + override def broadcast(hash: BlockHash): Unit = { + (for { + payloadWithChannel <- Option(lastPayloadMessages.get(hash)).toRight(s"No prepared for broadcast payload $hash") + _ <- Either.cond(hash == payloadWithChannel.pm.hash, (), s"Payload for block $hash is not last received") + _ = log.debug(s"Broadcasting block ${payloadWithChannel.pm.hash} payload") + _ <- Try(allChannels.broadcast(payloadWithChannel.pm, Some(payloadWithChannel.ch))).toEither.leftMap(_.getMessage) + } yield ()).fold( + err => log.error(s"Failed to broadcast last received payload: $err"), + identity + ) + + lastPayloadMessages.remove(hash) + } + + def updateMinerPublicKeys(newKeys: Map[EthAddress, PublicKey]): Unit = { + addrToPK.clear() + addrToPK.putAll(newKeys.asJava) + } + + // TODO: remove Task + private def requestFromNextChannel(req: BlockHash, candidate: Option[Channel], excluded: Set[Channel]): Task[Unit] = Task { + candidate.filterNot(excluded).orElse(nextOpenChannel(excluded)) match { + case None => + log.trace(s"No channel to request $req") + Set.empty[Channel] + case Some(ch) => + ch.writeAndFlush(GetPayload(req)) + excluded ++ Set(ch) + } + }.flatMap(newExcluded => requestFromNextChannel(req, candidate, newExcluded).delayExecution(syncTimeout)) + + private def nextOpenChannel(exclude: Set[Channel]): Option[Channel] = + allChannels.asScala.find(c => !exclude(c) && c.isOpen) + +} + +object PayloadObserverImpl { + sealed trait State + + object State { + case class LoadingPayload(blockHash: BlockHash, nextAttempt: Cancelable, promise: CancelablePromise[ExecutionPayloadInfo]) extends State + + case class Idle(pinnedChannel: Option[Channel]) extends State + } + +} diff --git a/src/main/scala/units/network/TrafficLogger.scala b/src/main/scala/units/network/TrafficLogger.scala index c26a62c9..1867dff9 100644 --- a/src/main/scala/units/network/TrafficLogger.scala +++ b/src/main/scala/units/network/TrafficLogger.scala @@ -2,7 +2,6 @@ package units.network import com.wavesplatform.network.{Handshake, HandshakeSpec, TrafficLogger as TL} import io.netty.channel.ChannelHandler.Sharable -import units.NetworkL2Block import units.network.BasicMessagesRepo.specsByCodes @Sharable @@ -12,18 +11,18 @@ class TrafficLogger(settings: TL.Settings) extends TL(settings) { protected def codeOf(msg: AnyRef): Option[Byte] = { val aux: PartialFunction[AnyRef, Byte] = { - case x: RawBytes => x.code - case _: NetworkL2Block => BlockSpec.messageCode - case x: Message => specsByClasses(x.getClass).messageCode - case _: Handshake => HandshakeSpec.messageCode + case x: RawBytes => x.code + case _: PayloadMessage => PayloadSpec.messageCode + case x: Message => specsByClasses(x.getClass).messageCode + case _: Handshake => HandshakeSpec.messageCode } aux.lift(msg) } protected def stringify(msg: Any): String = msg match { - case b: NetworkL2Block => s"${b.hash}" + case pm: PayloadMessage => s"PayloadMessage(hash=${pm.hash})" case RawBytes(code, data) => s"RawBytes(${specsByCodes(code).messageName}, ${data.length} bytes)" - case other => other.toString + case other => other.toString } } diff --git a/src/main/scala/units/util/HexBytesConverter.scala b/src/main/scala/units/util/HexBytesConverter.scala index d939087d..ae7721a0 100644 --- a/src/main/scala/units/util/HexBytesConverter.scala +++ b/src/main/scala/units/util/HexBytesConverter.scala @@ -1,7 +1,6 @@ package units.util import com.wavesplatform.common.state.ByteStr -import units.BlockHash import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.utils.Numeric @@ -9,9 +8,6 @@ import java.math.BigInteger object HexBytesConverter { - def toByteStr(hash: BlockHash): ByteStr = - ByteStr(toBytes(hash)) - def toInt(intHex: String): Int = Numeric.toBigInt(intHex).intValueExact() diff --git a/src/test/scala/units/BaseIntegrationTestSuite.scala b/src/test/scala/units/BaseIntegrationTestSuite.scala index c94b4e42..a208ec10 100644 --- a/src/test/scala/units/BaseIntegrationTestSuite.scala +++ b/src/test/scala/units/BaseIntegrationTestSuite.scala @@ -37,7 +37,7 @@ trait BaseIntegrationTestSuite val txs = List( d.chainContract.setScript(), - d.chainContract.setup(d.ecGenesisBlock, elMinerDefaultReward.amount.longValue()) + d.chainContract.setup(d.genesisBlockPayload, elMinerDefaultReward.amount.longValue()) ) ++ settings.initialMiners.map { x => d.chainContract.join(x.account, x.elRewardAddress) } diff --git a/src/test/scala/units/BlockBriefValidationTestSuite.scala b/src/test/scala/units/BlockBriefValidationTestSuite.scala index ada4ebf5..5dcd3ffd 100644 --- a/src/test/scala/units/BlockBriefValidationTestSuite.scala +++ b/src/test/scala/units/BlockBriefValidationTestSuite.scala @@ -11,29 +11,29 @@ class BlockBriefValidationTestSuite extends BaseIntegrationTestSuite { initialMiners = List(miner) ) - "Brief validation of EC Block incoming from network" - { + "Brief validation of payload" - { "accepts if it is valid" in test { d => - val ecBlock = d.createEcBlockBuilder("0", miner).build() + val payload = d.createPayloadBuilder("0", miner).build() - step(s"Receive ecBlock ${ecBlock.hash} from a peer") - d.receiveNetworkBlock(ecBlock, miner.account) - withClue("Brief EL block validation:") { + step(s"Receive block ${payload.hash} payload from a peer") + d.receivePayload(payload, miner.account) + withClue("Brief payload validation:") { d.triggerScheduledTasks() - d.pollSentNetworkBlock() match { - case Some(sent) => sent.hash shouldBe ecBlock.hash - case None => fail(s"${ecBlock.hash} should not be ignored") + d.pollSentPayloadMessage() match { + case Some(sent) => sent.hash shouldBe payload.hash + case None => fail(s"${payload.hash} should not be ignored") } } } "otherwise ignoring" in test { d => - val ecBlock = d.createEcBlockBuilder("0", minerRewardL2Address = EthAddress.empty, parent = d.ecGenesisBlock).build() + val payload = d.createPayloadBuilder("0", minerRewardAddress = EthAddress.empty, parentPayload = d.genesisBlockPayload).build() - step(s"Receive ecBlock ${ecBlock.hash} from a peer") - d.receiveNetworkBlock(ecBlock, miner.account) - withClue("Brief EL block validation:") { + step(s"Receive block ${payload.hash} payload from a peer") + d.receivePayload(payload, miner.account) + withClue("Brief payload validation:") { d.triggerScheduledTasks() - if (d.pollSentNetworkBlock().nonEmpty) fail(s"${ecBlock.hash} should be ignored, because it is invalid by brief validation rules") + if (d.pollSentPayloadMessage().nonEmpty) fail(s"${payload.hash} should be ignored, because it is invalid by brief validation rules") } } } diff --git a/src/test/scala/units/BlockFullValidationTestSuite.scala b/src/test/scala/units/BlockFullValidationTestSuite.scala index 643f029c..65ab7b19 100644 --- a/src/test/scala/units/BlockFullValidationTestSuite.scala +++ b/src/test/scala/units/BlockFullValidationTestSuite.scala @@ -4,14 +4,14 @@ import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.transaction.TxHelpers import units.ELUpdater.State.ChainStatus.{FollowingChain, WaitForNewChain} import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex -import units.client.engine.model.{EcBlock, GetLogsResponseEntry} +import units.client.engine.model.{ExecutionPayload, GetLogsResponseEntry} import units.eth.EthAddress import units.util.HexBytesConverter class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { private val transferEvents = List(Bridge.ElSentNativeEvent(TxHelpers.defaultAddress, 1)) - private val ecBlockLogs = transferEvents.map(getLogsResponseEntry) - private val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(ecBlockLogs).explicitGet()) + private val blockLogs = transferEvents.map(getLogsResponseEntry) + private val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(blockLogs).explicitGet()) private val reliable = ElMinerSettings(TxHelpers.signer(1)) private val malfunction = ElMinerSettings(TxHelpers.signer(2)) // Prevents a block finalization @@ -22,17 +22,17 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { "Full validation when the block is available on EL and CL" - { "doesn't happen for finalized blocks" in withExtensionDomain(defaultSettings.copy(initialMiners = List(reliable))) { d => - step("Start new epoch for ecBlock") + step("Start new epoch for payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) d.advanceConsensusLayerChanged() - step(s"Receive ecBlock ${ecBlock.hash} from a peer") - d.receiveNetworkBlock(ecBlock, reliable.account) + step(s"Receive block ${payload.hash} payload from a peer") + d.receivePayload(payload, reliable.account) d.triggerScheduledTasks() - step(s"Append a CL micro block with ecBlock ${ecBlock.hash} confirmation") - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock)) + step(s"Append a CL micro block with block ${payload.hash} confirmation") + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload)) d.advanceConsensusLayerChanged() withClue("Validation doesn't happen:") { @@ -41,7 +41,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { d.waitForWorking("Block considered validated and following") { s => val vs = s.fullValidationStatus - vs.lastValidatedBlock.hash shouldBe ecBlock.hash + vs.lastValidatedBlock.hash shouldBe payload.hash vs.lastElWithdrawalIndex shouldBe empty is[FollowingChain](s.chainStatus) @@ -50,28 +50,28 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { "happens for not finalized blocks" - { "successful validation updates the chain information" in withExtensionDomain() { d => - step("Start new epoch for ecBlock") + step("Start new epoch for payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) d.advanceConsensusLayerChanged() - step(s"Receive ecBlock ${ecBlock.hash} from a peer") - d.receiveNetworkBlock(ecBlock, reliable.account) + step(s"Receive block ${payload.hash} payload from a peer") + d.receivePayload(payload, reliable.account) d.triggerScheduledTasks() - step(s"Append a CL micro block with ecBlock ${ecBlock.hash} confirmation") - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + step(s"Append a CL micro block with block ${payload.hash} confirmation") + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() d.waitForCS[FollowingChain]("Following chain") { _ => } withClue("Validation happened:") { - d.ecClients.fullValidatedBlocks shouldBe Set(ecBlock.hash) + d.ecClients.fullValidatedBlocks shouldBe Set(payload.hash) } d.waitForWorking("Block considered validated") { s => val vs = s.fullValidationStatus - vs.lastValidatedBlock.hash shouldBe ecBlock.hash + vs.lastValidatedBlock.hash shouldBe payload.hash vs.lastElWithdrawalIndex.value shouldBe -1L } } @@ -80,54 +80,55 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { def e2CTest( blockLogs: List[GetLogsResponseEntry], e2CTransfersRootHashHex: String, - badBlockPostProcessing: EcBlock => EcBlock = identity + badBlockPayloadPostProcessing: ExecutionPayload => ExecutionPayload = identity ): Unit = withExtensionDomain() { d => - step("Start new epoch for ecBlock1") + step("Start new epoch for payload1") d.advanceNewBlocks(malfunction.address) d.advanceConsensusLayerChanged() - val ecBlock1 = d.createEcBlockBuilder("0", malfunction).buildAndSetLogs() - d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock1)) + val payload1 = d.createPayloadBuilder("0", malfunction).buildAndSetLogs() + d.ecClients.addKnown(payload1) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, payload1)) d.advanceConsensusLayerChanged() - step("Start new epoch for ecBlock2") + step("Start new epoch for payload2") d.advanceNewBlocks(malfunction.address) d.advanceConsensusLayerChanged() - val ecBlock2 = badBlockPostProcessing(d.createEcBlockBuilder("0-0", malfunction, ecBlock1).rewardPrevMiner().buildAndSetLogs(blockLogs)) + val payload2 = + badBlockPayloadPostProcessing(d.createPayloadBuilder("0-0", malfunction, payload1).rewardPrevMiner().buildAndSetLogs(blockLogs)) - step(s"Append a CL micro block with ecBlock2 ${ecBlock2.hash} confirmation") - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock2, e2CTransfersRootHashHex)) + step(s"Append a CL micro block with block2 ${payload2.hash} confirmation") + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, payload2, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() - step(s"Receive ecBlock2 ${ecBlock2.hash} from a peer") - d.receiveNetworkBlock(ecBlock2, malfunction.account) + step(s"Receive block2 ${payload2.hash} payload2 from a peer") + d.receivePayload(payload2, malfunction.account) d.triggerScheduledTasks() d.waitForCS[WaitForNewChain]("Forking") { cs => - cs.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash + cs.chainSwitchInfo.referenceBlock.hash shouldBe payload1.hash } } "CL confirmation without a transfers root hash" in e2CTest( - blockLogs = ecBlockLogs, + blockLogs = blockLogs, e2CTransfersRootHashHex = EmptyE2CTransfersRootHashHex ) "Events from an unexpected EL bridge address" in { val fakeBridgeAddress = EthAddress.unsafeFrom("0x53481054Ad294207F6ed4B6C2E6EaE34E1Bb8704") - val ecBlock2Logs = transferEvents.map(x => getLogsResponseEntry(x).copy(address = fakeBridgeAddress)) + val block2Logs = transferEvents.map(x => getLogsResponseEntry(x).copy(address = fakeBridgeAddress)) e2CTest( - blockLogs = ecBlock2Logs, + blockLogs = block2Logs, e2CTransfersRootHashHex = e2CTransfersRootHashHex ) } "Different miners in CL and EL" in e2CTest( - blockLogs = ecBlockLogs, + blockLogs = blockLogs, e2CTransfersRootHashHex = e2CTransfersRootHashHex, - badBlockPostProcessing = _.copy(minerRewardL2Address = reliable.elRewardAddress) + badBlockPayloadPostProcessing = _.copy(feeRecipient = reliable.elRewardAddress) ) } } diff --git a/src/test/scala/units/BlockIssuesForgingTestSuite.scala b/src/test/scala/units/BlockIssuesForgingTestSuite.scala index f581ac79..81b9a0c3 100644 --- a/src/test/scala/units/BlockIssuesForgingTestSuite.scala +++ b/src/test/scala/units/BlockIssuesForgingTestSuite.scala @@ -4,9 +4,9 @@ import com.wavesplatform.db.WithState.AddrWithBalance import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.wallet.Wallet import units.ELUpdater.State.ChainStatus.{FollowingChain, Mining, WaitForNewChain} -import units.ELUpdater.WaitRequestedBlockTimeout +import units.ELUpdater.WaitRequestedPayloadTimeout import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees -import units.client.engine.model.EcBlock +import units.client.engine.model.ExecutionPayload import scala.concurrent.duration.DurationInt @@ -25,164 +25,164 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { .withEnabledElMining "We're on the main chain and" - { - def test(f: (ExtensionDomain, EcBlock, Int) => Unit): Unit = withExtensionDomain() { d => - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBlock1") + def test(f: (ExtensionDomain, ExecutionPayload, Int) => Unit): Unit = withExtensionDomain() { d => + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with payload1") d.advanceNewBlocks(otherMiner1.address) - val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() - d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + val payload1 = d.createPayloadBuilder("0", otherMiner1).buildAndSetLogs() + d.ecClients.addKnown(payload1) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, payload1)) d.waitForCS[FollowingChain]() { s => - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock1.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload1.hash } - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBlock2") + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with payload2") d.advanceNewBlocks(otherMiner1.address) - val ecBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - val ecBlock2Epoch = d.blockchain.height - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock2)) + val payload2 = d.createPayloadBuilder("0-0", otherMiner1, payload1).rewardPrevMiner().buildAndSetLogs() + val block2Epoch = d.blockchain.height + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, payload2)) - d.waitForCS[FollowingChain](s"Waiting ecBlock2 ${ecBlock2.hash}") { s => - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock2.hash + d.waitForCS[FollowingChain](s"Waiting payload2 ${payload2.hash}") { s => + s.nodeChainInfo.lastBlock.hash shouldBe payload2.hash + s.nextExpectedBlock.map(_.hash).value shouldBe payload2.hash } step(s"Start a new epoch of thisMiner ${thisMiner.address}") d.advanceNewBlocks(thisMiner.address) - f(d, ecBlock2, ecBlock2Epoch) + f(d, payload2, block2Epoch) } - "EC-block comes within timeout - then we continue forging" in test { (d, ecBlock2, ecBlock2Epoch) => - d.advanceElu(WaitRequestedBlockTimeout - 1.millis) - d.waitForCS[FollowingChain](s"Still waiting ecBlock2 ${ecBlock2.hash}") { s => - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock2.hash + "Block payload comes within timeout - then we continue forging" in test { (d, payload2, block2Epoch) => + d.advanceElu(WaitRequestedPayloadTimeout - 1.millis) + d.waitForCS[FollowingChain](s"Still waiting payload2 ${payload2.hash}") { s => + s.nextExpectedBlock.map(_.hash).value shouldBe payload2.hash } - step(s"Receive EC-block ${ecBlock2.hash} from network") - d.receiveNetworkBlock(ecBlock2, otherMiner1.account, ecBlock2Epoch) + step(s"Receive block2 ${payload2.hash} payload2") + d.receivePayload(payload2, otherMiner1.account, block2Epoch) d.triggerScheduledTasks() - d.ecClients.willForge(d.createEcBlockBuilder("0-0-0", otherMiner1, ecBlock2).rewardPrevMiner().build()) + d.ecClients.willForge(d.createPayloadBuilder("0-0-0", otherMiner1, payload2).rewardPrevMiner().build()) d.waitForCS[Mining]("Continue") { s => s.nodeChainInfo.isRight shouldBe true } } - "EC-block doesn't come - then we start an alternative chain" in test { (d, _, _) => + "Block payload doesn't come - then we start an alternative chain" in test { (d, _, _) => d.waitForCS[WaitForNewChain](s"Switched to alternative chain") { _ => } } } "We're on the alternative chain and" - { - "EC-block comes within timeout - then we continue forging" in withExtensionDomain() { d => - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBlock1") + "Block payload comes within timeout - then we continue forging" in withExtensionDomain() { d => + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with payload1") d.advanceNewBlocks(otherMiner1.address) - val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() - d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + val payload1 = d.createPayloadBuilder("0", otherMiner1).buildAndSetLogs() + d.ecClients.addKnown(payload1) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, payload1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock1.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload1.hash } - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2") + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with badPayload2") d.advanceNewBlocks(otherMiner1.address) - val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) + val badPayload2 = d.createPayloadBuilder("0-0", otherMiner1, payload1).rewardPrevMiner().buildAndSetLogs() + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, badPayload2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true - s.nodeChainInfo.lastBlock.hash shouldBe ecBadBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe badPayload2.hash } - step(s"Start a new epoch of otherMiner2 ${otherMiner2.address} with alternative chain ecBlock2") + step(s"Start a new epoch of otherMiner2 ${otherMiner2.address} with alternative chain payload2") d.advanceNewBlocks(otherMiner2.address) - val ecBlock2 = d.createEcBlockBuilder("0-1", otherMiner2, ecBlock1).rewardPrevMiner().buildAndSetLogs() + val payload2 = d.createPayloadBuilder("0-1", otherMiner2, payload1).rewardPrevMiner().buildAndSetLogs() d.waitForCS[WaitForNewChain]() { s => - s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash + s.chainSwitchInfo.referenceBlock.hash shouldBe payload1.hash } - d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, ecBlock2)) + d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, payload2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload2.hash + s.nextExpectedBlock.map(_.hash).value shouldBe payload2.hash } - d.receiveNetworkBlock(ecBlock2, otherMiner2.account, d.blockchain.height) + d.receivePayload(payload2, otherMiner2.account, d.blockchain.height) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload2.hash s.nextExpectedBlock shouldBe empty } - step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBlock3") + step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with payload3") d.advanceNewBlocks(otherMiner2.address) - val ecBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, parent = ecBlock2).rewardPrevMiner(1).buildAndSetLogs() - val ecBlock3Epoch = d.blockchain.height - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1)) + val payload3 = d.createPayloadBuilder("0-1-1", otherMiner2, parentPayload = payload2).rewardPrevMiner(1).buildAndSetLogs() + val block3Epoch = d.blockchain.height + d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, payload3, chainId = 1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock3.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock3.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload3.hash + s.nextExpectedBlock.map(_.hash).value shouldBe payload3.hash } step(s"Start a new epoch of thisMiner ${thisMiner.address}") d.advanceNewBlocks(thisMiner.address) d.advanceConsensusLayerChanged() - d.advanceElu(WaitRequestedBlockTimeout - 1.millis) + d.advanceElu(WaitRequestedPayloadTimeout - 1.millis) - d.waitForCS[FollowingChain](s"Waiting ecBlock3 ${ecBlock3.hash}") { s => + d.waitForCS[FollowingChain](s"Waiting payload3 ${payload3.hash}") { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock3.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock3.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload3.hash + s.nextExpectedBlock.map(_.hash).value shouldBe payload3.hash } - step(s"Receive ecBlock3 ${ecBlock3.hash}") - d.receiveNetworkBlock(ecBlock3, thisMiner.account, ecBlock3Epoch) + step(s"Receive block3 ${payload3.hash} payload3") + d.receivePayload(payload3, thisMiner.account, block3Epoch) - d.ecClients.willForge(d.createEcBlockBuilder("0-1-1-1", thisMiner, ecBlock3).rewardPrevMiner(2).build()) + d.ecClients.willForge(d.createPayloadBuilder("0-1-1-1", thisMiner, payload3).rewardPrevMiner(2).build()) d.waitForCS[Mining]() { s => s.nodeChainInfo.value.isMain shouldBe false } } - "We mined before the alternative chain before and EC-block doesn't come - then we still wait for it" in withExtensionDomain() { d => - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBlock1") + "We mined before the alternative chain before and block payload doesn't come - then we still wait for it" in withExtensionDomain() { d => + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with payload1") d.advanceNewBlocks(otherMiner1.address) - val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() - d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + val payload1 = d.createPayloadBuilder("0", otherMiner1).buildAndSetLogs() + d.ecClients.addKnown(payload1) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, payload1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock1.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload1.hash } - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2") + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with badPayload2") d.advanceNewBlocks(otherMiner1.address) - val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) + val badPayload2 = d.createPayloadBuilder("0-0", otherMiner1, payload1).rewardPrevMiner().buildAndSetLogs() + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, badPayload2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true - s.nodeChainInfo.lastBlock.hash shouldBe ecBadBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe badPayload2.hash } - step(s"Start a new epoch of thisMiner ${thisMiner.address} with alternative chain ecBlock2") + step(s"Start a new epoch of thisMiner ${thisMiner.address} with alternative chain payload2") d.advanceNewBlocks(thisMiner.address) - val ecBlock2 = d.createEcBlockBuilder("0-1", thisMiner, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.ecClients.willForge(ecBlock2) - d.ecClients.willForge(d.createEcBlockBuilder("0-1-i", thisMiner, ecBlock2).buildAndSetLogs()) + val payload2 = d.createPayloadBuilder("0-1", thisMiner, payload1).rewardPrevMiner().buildAndSetLogs() + d.ecClients.willForge(payload2) + d.ecClients.willForge(d.createPayloadBuilder("0-1-i", thisMiner, payload2).buildAndSetLogs()) d.waitForCS[Mining]() { s => val ci = s.nodeChainInfo.left.value - ci.referenceBlock.hash shouldBe ecBlock1.hash + ci.referenceBlock.hash shouldBe payload1.hash } d.advanceMining() @@ -190,94 +190,94 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.waitForCS[Mining]() { s => val ci = s.nodeChainInfo.value - ci.lastBlock.hash shouldBe ecBlock2.hash + ci.lastBlock.hash shouldBe payload2.hash } - step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBadBlock3") + step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with badPayload3") d.advanceNewBlocks(otherMiner2.address) - val ecBadBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, ecBlock2).rewardMiner(otherMiner2.elRewardAddress, 1).buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBadBlock3, chainId = 1)) + val badPayload3 = d.createPayloadBuilder("0-1-1", otherMiner2, payload2).rewardMiner(otherMiner2.elRewardAddress, 1).buildAndSetLogs() + d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, badPayload3, chainId = 1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBadBlock3.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBadBlock3.hash + s.nodeChainInfo.lastBlock.hash shouldBe badPayload3.hash + s.nextExpectedBlock.map(_.hash).value shouldBe badPayload3.hash } step(s"Continue an alternative chain by thisMiner ${thisMiner.address}") d.advanceNewBlocks(thisMiner.address) - d.advanceWaitRequestedBlock() - d.advanceWaitRequestedBlock() + d.advanceWaitRequestedBlockPayload() + d.advanceWaitRequestedBlockPayload() - d.waitForCS[FollowingChain](s"Still wait for ecBadBlock3 ${ecBadBlock3.hash}") { s => + d.waitForCS[FollowingChain](s"Still wait for badPayload3 ${badPayload3.hash}") { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBadBlock3.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBadBlock3.hash + s.nodeChainInfo.lastBlock.hash shouldBe badPayload3.hash + s.nextExpectedBlock.map(_.hash).value shouldBe badPayload3.hash } } - "We haven't mined the alternative chain before and EC-block doesn't come - then we wait for a new alternative chain" in + "We haven't mined the alternative chain before and block payload doesn't come - then we wait for a new alternative chain" in withExtensionDomain() { d => - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBlock1") + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with payload1") d.advanceNewBlocks(otherMiner1.address) - val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() - d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + val payload1 = d.createPayloadBuilder("0", otherMiner1).buildAndSetLogs() + d.ecClients.addKnown(payload1) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, payload1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock1.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload1.hash } - step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2") + step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with badPayload2") d.advanceNewBlocks(otherMiner1.address) - val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) + val badPayload2 = d.createPayloadBuilder("0-0", otherMiner1, payload1).rewardPrevMiner().buildAndSetLogs() + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, badPayload2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true - s.nodeChainInfo.lastBlock.hash shouldBe ecBadBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe badPayload2.hash } - step(s"Start a new epoch of otherMiner2 ${otherMiner2.address} with alternative chain ecBlock2") + step(s"Start a new epoch of otherMiner2 ${otherMiner2.address} with alternative chain payload2") d.advanceNewBlocks(otherMiner2.address) - val ecBlock2 = d.createEcBlockBuilder("0-1", otherMiner2, ecBlock1).rewardPrevMiner().buildAndSetLogs() + val payload2 = d.createPayloadBuilder("0-1", otherMiner2, payload1).rewardPrevMiner().buildAndSetLogs() d.waitForCS[WaitForNewChain]() { s => - s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash + s.chainSwitchInfo.referenceBlock.hash shouldBe payload1.hash } - d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, ecBlock2)) + d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, payload2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload2.hash + s.nextExpectedBlock.map(_.hash).value shouldBe payload2.hash } - d.receiveNetworkBlock(ecBlock2, otherMiner2.account, d.blockchain.height) + d.receivePayload(payload2, otherMiner2.account, d.blockchain.height) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload2.hash s.nextExpectedBlock shouldBe empty } - step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBlock3") + step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with payload3") d.advanceNewBlocks(otherMiner2.address) - val ecBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, ecBlock2).rewardPrevMiner(1).buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1)) + val payload3 = d.createPayloadBuilder("0-1-1", otherMiner2, payload2).rewardPrevMiner(1).buildAndSetLogs() + d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, payload3, chainId = 1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false - s.nodeChainInfo.lastBlock.hash shouldBe ecBlock3.hash - s.nextExpectedBlock.map(_.hash).value shouldBe ecBlock3.hash + s.nodeChainInfo.lastBlock.hash shouldBe payload3.hash + s.nextExpectedBlock.map(_.hash).value shouldBe payload3.hash } step(s"Start a new epoch of thisMiner ${thisMiner.address}") d.advanceNewBlocks(thisMiner.address) d.waitForCS[WaitForNewChain]() { s => - s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash + s.chainSwitchInfo.referenceBlock.hash shouldBe payload1.hash } } } diff --git a/src/test/scala/units/E2CTransfersTestSuite.scala b/src/test/scala/units/E2CTransfersTestSuite.scala index 6881ccf5..612acfcc 100644 --- a/src/test/scala/units/E2CTransfersTestSuite.scala +++ b/src/test/scala/units/E2CTransfersTestSuite.scala @@ -21,8 +21,8 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { private val transferReceiver = TxHelpers.secondSigner private val transfer = Bridge.ElSentNativeEvent(transferReceiver.toAddress, 1) private val transferEvent = getLogsResponseEntry(transfer) - private val ecBlockLogs = List(transferEvent) - private val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(ecBlockLogs).explicitGet()) + private val blockLogs = List(transferEvent) + private val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(blockLogs).explicitGet()) private val transferProofs = Bridge.mkTransferProofs(List(transfer), 0).reverse // Contract requires from bottom to top private val reliable = ElMinerSettings(Wallet.generateNewAccount(TestSettings.Default.walletSeed, 0)) @@ -39,8 +39,8 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { val transfer1 = Bridge.ElSentNativeEvent(transferReceiver1.toAddress, 1) val transfer2 = Bridge.ElSentNativeEvent(transferReceiver2.toAddress, 1) val transferEvents = List(transfer1, transfer2) - val ecBlockLogs = transferEvents.map(getLogsResponseEntry) - val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(ecBlockLogs).explicitGet()) + val blockLogs = transferEvents.map(getLogsResponseEntry) + val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(blockLogs).explicitGet()) val transfer1Proofs = Bridge.mkTransferProofs(transferEvents, 0).reverse val transfer2Proofs = Bridge.mkTransferProofs(transferEvents, 1).reverse @@ -52,20 +52,20 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { ) withExtensionDomain(settings) { d => - step(s"Start new epoch for ecBlock") + step(s"Start new epoch for payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) def tryWithdraw(): Either[Throwable, BlockId] = d.appendMicroBlockE( - d.chainContract.withdraw(transferReceiver1, ecBlock, transfer1Proofs, 0, transfer1.amount), - d.chainContract.withdraw(transferReceiver2, ecBlock, transfer2Proofs, 1, transfer2.amount) + d.chainContract.withdraw(transferReceiver1, payload, transfer1Proofs, 0, transfer1.amount), + d.chainContract.withdraw(transferReceiver2, payload, transfer2Proofs, 1, transfer2.amount) ) tryWithdraw() should produce("not found for the contract address") - step("Append a CL micro block with ecBlock confirmation") - d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + step("Append a CL micro block with block confirmation") + d.ecClients.addKnown(payload) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() tryWithdraw() should beRight @@ -88,15 +88,15 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { ) ) { case (index, errorMessage) => withExtensionDomain() { d => - step(s"Start new epoch with ecBlock") + step(s"Start new epoch with payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) - d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) + d.ecClients.addKnown(payload) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, index, transfer.amount)) + d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, payload, transferProofs, index, transfer.amount)) tryWithdraw() should produce(errorMessage) } @@ -110,30 +110,30 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { ) ) { amount => withExtensionDomain() { d => - step(s"Start new epoch with ecBlock") + step(s"Start new epoch with payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) - d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) + d.ecClients.addKnown(payload) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, amount)) + d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, payload, transferProofs, 0, amount)) tryWithdraw() should produce("Amount should be positive") } } "Can't get transferred tokens if the data is incorrect and able if it is correct" in withExtensionDomain() { d => - step(s"Start new epoch with ecBlock") + step(s"Start new epoch with payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, payload, transferProofs, 0, transfer.amount)) tryWithdraw() should produce("not found for the contract address") - d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.ecClients.addKnown(payload) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() tryWithdraw() should beRight @@ -149,15 +149,15 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { ) withExtensionDomain(settings) { d => - step(s"Start new epoch with ecBlock") + step(s"Start new epoch with payload") d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) + val payload = d.createPayloadBuilder("0", reliable).buildAndSetLogs(blockLogs) def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, payload, transferProofs, 0, transfer.amount)) tryWithdraw() should produce("not found for the contract address") - d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.ecClients.addKnown(payload) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, payload, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() tryWithdraw() should beRight @@ -182,13 +182,13 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { ) ) { transferEvent => withExtensionDomain(settings) { d => - step(s"Start new epoch with ecBlock1") + step(s"Start new epoch with payload1") d.advanceNewBlocks(reliable.address) - val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent)) + val payload1 = d.createPayloadBuilder("0", reliable).buildAndSetLogs(List(transferEvent)) def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock1, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, payload1, transferProofs, 0, transfer.amount)) - d.ecClients.willForge(ecBlock1) + d.ecClients.willForge(payload1) d.advanceConsensusLayerChanged() d.advanceMining() @@ -207,11 +207,11 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { "Fails on wrong data" in { val settings = defaultSettings.withEnabledElMining withExtensionDomain(settings) { d => - step(s"Start new epoch with ecBlock1") + step(s"Start new epoch with payload1") d.advanceNewBlocks(reliable.address) - val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent.copy(data = "d3ad884fa04292"))) - d.ecClients.willForge(ecBlock1) - d.ecClients.willForge(d.createEcBlockBuilder("0-0", reliable).build()) + val payload1 = d.createPayloadBuilder("0", reliable).buildAndSetLogs(List(transferEvent.copy(data = "d3ad884fa04292"))) + d.ecClients.willForge(payload1) + d.ecClients.willForge(d.createPayloadBuilder("0-0", reliable).build()) d.advanceConsensusLayerChanged() @@ -223,50 +223,50 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { "Can't get transferred tokens from a fork and can after the fork becomes a main chain" in { val settings = defaultSettings.copy(initialMiners = List(reliable, malfunction)).withEnabledElMining withExtensionDomain(settings) { d => - step(s"Start a new epoch of malfunction miner ${malfunction.address} with ecBlock1") + step(s"Start a new epoch of malfunction miner ${malfunction.address} with payload1") d.advanceNewBlocks(malfunction.address) // Need this block, because we can not rollback to the genesis block - val ecBlock1 = d.createEcBlockBuilder("0", malfunction).buildAndSetLogs() + val payload1 = d.createPayloadBuilder("0", malfunction).buildAndSetLogs() d.advanceConsensusLayerChanged() - d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock1)) + d.ecClients.addKnown(payload1) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, payload1)) d.advanceConsensusLayerChanged() step(s"Try to append a block with a wrong transfers root hash") d.advanceNewBlocks(malfunction.address) - val ecBadBlock2 = d.createEcBlockBuilder("0-0", malfunction, ecBlock1).rewardPrevMiner().buildAndSetLogs(ecBlockLogs) + val badPayload2 = d.createPayloadBuilder("0-0", malfunction, payload1).rewardPrevMiner().buildAndSetLogs(blockLogs) d.advanceConsensusLayerChanged() // No root hash in extendMainChain tx - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBadBlock2)) // No root hash - d.receiveNetworkBlock(ecBadBlock2, malfunction.account) + d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, badPayload2)) // No root hash + d.receivePayload(badPayload2, malfunction.account) d.advanceConsensusLayerChanged() d.waitForCS[WaitForNewChain]("State is expected") { s => - s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash + s.chainSwitchInfo.referenceBlock.hash shouldBe payload1.hash } - step(s"Start an alternative chain by a reliable miner ${reliable.address} with ecBlock2") + step(s"Start an alternative chain by a reliable miner ${reliable.address} with payload2") d.advanceNewBlocks(reliable.address) - val ecBlock2 = d.createEcBlockBuilder("0-1", reliable, ecBlock1).rewardPrevMiner().buildAndSetLogs(ecBlockLogs) - d.ecClients.willForge(ecBlock2) + val payload2 = d.createPayloadBuilder("0-1", reliable, payload1).rewardPrevMiner().buildAndSetLogs(blockLogs) + d.ecClients.willForge(payload2) // Prepare a following block, because we start mining it immediately - d.ecClients.willForge(d.createEcBlockBuilder("0-1-1", reliable, ecBlock2).build()) + d.ecClients.willForge(d.createPayloadBuilder("0-1-1", reliable, payload2).build()) d.advanceConsensusLayerChanged() d.waitForCS[Mining]("State is expected") { s => - s.nodeChainInfo.left.value.referenceBlock.hash shouldBe ecBlock1.hash + s.nodeChainInfo.left.value.referenceBlock.hash shouldBe payload1.hash } d.advanceMining() d.waitForCS[Mining]("State is expected") { s => - s.nodeChainInfo.left.value.referenceBlock.hash shouldBe ecBlock1.hash + s.nodeChainInfo.left.value.referenceBlock.hash shouldBe payload1.hash } step(s"Confirm startAltChain and append with new blocks and remove a malfunction miner") d.appendMicroBlockAndVerify( - d.chainContract.startAltChain(reliable.account, ecBlock2, e2CTransfersRootHashHex), + d.chainContract.startAltChain(reliable.account, payload2, e2CTransfersRootHashHex), d.chainContract.leave(malfunction.account) ) d.advanceConsensusLayerChanged() @@ -274,21 +274,21 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { d.waitForCS[Mining]("State is expected") { _ => } def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock2, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, payload2, transferProofs, 0, transfer.amount)) withClue("Can't withdraw from a fork:") { tryWithdraw() should produce("is not finalized") } - step(s"Moving whole network to the alternative chain with ecBlock3") + step(s"Moving whole network to the alternative chain with payload3") d.advanceNewBlocks(reliable.address) - val ecBlock3 = d.createEcBlockBuilder("0-1-1-1", reliable, ecBlock2).rewardPrevMiner(1).buildAndSetLogs() - d.ecClients.willForge(ecBlock3) + val payload3 = d.createPayloadBuilder("0-1-1-1", reliable, payload2).rewardPrevMiner(1).buildAndSetLogs() + d.ecClients.willForge(payload3) d.advanceConsensusLayerChanged() step("Confirm extendAltChain to make this chain main") d.advanceMining() - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(reliable.account, ecBlock3, chainId = 1)) + d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(reliable.account, payload3, chainId = 1)) d.advanceConsensusLayerChanged() d.waitForCS[Mining]("State is expected") { _ => } diff --git a/src/test/scala/units/ExtensionDomain.scala b/src/test/scala/units/ExtensionDomain.scala index 7c7fa740..10959c0a 100644 --- a/src/test/scala/units/ExtensionDomain.scala +++ b/src/test/scala/units/ExtensionDomain.scala @@ -23,7 +23,6 @@ import com.wavesplatform.transaction.{DiscardedBlocks, Transaction} import com.wavesplatform.utils.{ScorexLogging, Time} import com.wavesplatform.utx.UtxPool import com.wavesplatform.wallet.Wallet -import io.netty.channel.Channel import io.netty.channel.embedded.EmbeddedChannel import io.netty.channel.group.DefaultChannelGroup import io.netty.util.concurrent.GlobalEventExecutor @@ -39,12 +38,12 @@ import play.api.libs.json.* import units.ELUpdater.* import units.ELUpdater.State.{ChainStatus, Working} import units.ExtensionDomain.* -import units.client.contract.HasConsensusLayerDappTxHelpers +import units.client.contract.{ChainContractStateClient, HasConsensusLayerDappTxHelpers} import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex -import units.client.engine.model.{EcBlock, TestEcBlocks} -import units.client.{L2BlockLike, TestEcClients} +import units.client.engine.model.{ExecutionPayload, TestPayloads} +import units.client.{CommonBlockData, TestEcClients} import units.eth.{EthAddress, EthereumConstants, Gwei} -import units.network.TestBlocksObserver +import units.network.{PayloadMessage, TestPayloadObserver} import units.test.CustomMatchers import java.nio.charset.StandardCharsets @@ -67,16 +66,16 @@ class ExtensionDomain( with ScorexLogging { self => override val chainContractAccount: KeyPair = KeyPair("chain-contract".getBytes(StandardCharsets.UTF_8)) - val l2Config = settings.config.as[ClientConfig]("waves.l2") + val l2Config: ClientConfig = settings.config.as[ClientConfig]("waves.l2") require(l2Config.chainContractAddress == chainContractAddress, "Check settings") - val ecGenesisBlock = EcBlock( - hash = TestEcBlockBuilder.createBlockHash(""), + val genesisBlockPayload: ExecutionPayload = ExecutionPayload( + hash = TestPayloadBuilder.createBlockHash(""), parentHash = BlockHash(EthereumConstants.EmptyBlockHashHex), // see main.ride stateRoot = EthereumConstants.EmptyRootHashHex, height = 0, timestamp = testTime.getTimestamp() / 1000 - l2Config.blockDelay.toSeconds, - minerRewardL2Address = EthAddress.empty, + feeRecipient = EthAddress.empty, baseFeePerGas = Uint256.DEFAULT, gasLimit = 0, gasUsed = 0, @@ -84,23 +83,24 @@ class ExtensionDomain( withdrawals = Vector.empty ) - val ecClients = new TestEcClients(ecGenesisBlock, blockchain) + val ecClients = new TestEcClients(genesisBlockPayload, blockchain) - val globalScheduler = TestScheduler(ExecutionModel.AlwaysAsyncExecution) - val eluScheduler = TestScheduler(ExecutionModel.AlwaysAsyncExecution) - - val elBlockStream = PublishSubject[(Channel, NetworkL2Block)]() - val blockObserver = new TestBlocksObserver(elBlockStream) + val globalScheduler: TestScheduler = TestScheduler(ExecutionModel.AlwaysAsyncExecution) + val eluScheduler: TestScheduler = TestScheduler(ExecutionModel.AlwaysAsyncExecution) val neighbourChannel = new EmbeddedChannel() val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE) allChannels.add(neighbourChannel) - def pollSentNetworkBlock(): Option[NetworkL2Block] = Option(neighbourChannel.readOutbound[NetworkL2Block]) - def receiveNetworkBlock(ecBlock: EcBlock, miner: SeedKeyPair, epochNumber: Int = blockchain.height): Unit = - receiveNetworkBlock(toNetworkBlock(ecBlock, miner, epochNumber)) - def receiveNetworkBlock(incomingNetworkBlock: NetworkL2Block): Unit = elBlockStream.onNext((new EmbeddedChannel(), incomingNetworkBlock)) - val extensionContext = new Context { + val payloadStream: PublishSubject[PayloadMessage] = PublishSubject[PayloadMessage]() + val payloadObserver: TestPayloadObserver = new TestPayloadObserver(payloadStream, allChannels) + + def pollSentPayloadMessage(): Option[PayloadMessage] = Option(neighbourChannel.readOutbound[PayloadMessage]) + def receivePayload(payload: ExecutionPayload, miner: SeedKeyPair, epochNumber: Int = blockchain.height): Unit = + receivePayload(toPayloadMessage(payload, miner, epochNumber)) + def receivePayload(incomingPayload: PayloadMessage): Unit = payloadStream.onNext(incomingPayload) + + val extensionContext: Context = new Context { override def settings: WavesSettings = self.settings override def blockchain: Blockchain = self.blockchain @@ -119,21 +119,23 @@ class ExtensionDomain( override def utxEvents: Observable[UtxEvent] = Observable.empty } + val chainContractClient = new ChainContractStateClient(chainContractAddress, blockchain) + val consensusClient: ConsensusClient = new ConsensusClient( l2Config, extensionContext, ecClients.engineApi, - blockObserver, - allChannels, + chainContractClient, + payloadObserver, globalScheduler, eluScheduler, () => {} ) triggers = triggers.appended(consensusClient) - val defaultMaxTimeout = - List(WaitForReferenceConfirmInterval, ClChangedProcessingDelay, MiningRetryInterval, WaitRequestedBlockTimeout).max + 1.millis - val defaultInterval = ClChangedProcessingDelay + val defaultMaxTimeout: FiniteDuration = + List(WaitForReferenceConfirmInterval, ClChangedProcessingDelay, MiningRetryInterval, WaitRequestedPayloadTimeout).max + 1.millis + val defaultInterval: FiniteDuration = ClChangedProcessingDelay def waitForWorking( title: String = "", @@ -169,19 +171,17 @@ class ExtensionDomain( f(is[CS](s.chainStatus)) } - def toNetworkBlock(ecBlock: EcBlock, miner: SeedKeyPair, epochNumber: Int): NetworkL2Block = - NetworkL2Block - .signed( - TestEcBlocks.toPayload( - ecBlock, - calculateRandao( - blockchain.vrf(epochNumber).getOrElse(throw new RuntimeException(s"VRF is empty for epoch $epochNumber")), - ecBlock.parentHash - ) - ), - miner.privateKey + def toPayloadMessage(payload: ExecutionPayload, miner: SeedKeyPair, epochNumber: Int): PayloadMessage = { + val payloadJson = TestPayloads.toPayloadJson( + payload, + calculateRandao( + blockchain.vrf(epochNumber).getOrElse(throw new RuntimeException(s"VRF is empty for epoch $epochNumber")), + payload.parentHash ) - .explicitGet() + ) + + PayloadMessage.signed(payloadJson, miner.privateKey).explicitGet() + } def forgeFromUtxPool(): Unit = { val (txsOpt, _, _) = utxPool.packUnconfirmed(MultiDimensionalMiningConstraint.Unlimited, None) @@ -234,7 +234,7 @@ class ExtensionDomain( def evaluateExtendAltChain( minerAccount: KeyPair, chainId: Long, - block: L2BlockLike, + blockData: CommonBlockData, epoch: Long, e2CTransfersRootHashHex: String = EmptyE2CTransfersRootHashHex ): Either[String, JsObject] = { @@ -244,8 +244,8 @@ class ExtensionDomain( "extendAltChain", List[Terms.EVALUATED]( Terms.CONST_LONG(chainId), - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), - Terms.CONST_STRING(block.parentHash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.parentHash.drop(2)).explicitGet(), Terms.CONST_LONG(epoch), Terms.CONST_STRING(e2CTransfersRootHashHex.drop(2)).explicitGet() ) @@ -292,8 +292,8 @@ class ExtensionDomain( // See ELUpdater.consensusLayerChanged def advanceConsensusLayerChanged(): Unit = advanceElu(ELUpdater.ClChangedProcessingDelay, "advanceConsensusLayerChanged") - // See ELUpdater.requestBlocksAndStartMining - def advanceWaitRequestedBlock(): Unit = advanceElu(ELUpdater.WaitRequestedBlockTimeout, "advanceWaitRequestedBlock") + // See ELUpdater.requestPayloadsAndStartMining + def advanceWaitRequestedBlockPayload(): Unit = advanceElu(ELUpdater.WaitRequestedPayloadTimeout, "advanceWaitRequestedBlockPayload") def advanceMiningRetry(): Unit = advanceElu(ELUpdater.MiningRetryInterval, "advanceMiningRetry") @@ -342,15 +342,15 @@ class ExtensionDomain( utxPool.close() } - def createEcBlockBuilder(hashPath: String, miner: ElMinerSettings, parent: EcBlock = ecGenesisBlock): TestEcBlockBuilder = - createEcBlockBuilder(hashPath, miner.elRewardAddress, parent) + def createPayloadBuilder(hashPath: String, miner: ElMinerSettings, parentPayload: ExecutionPayload = genesisBlockPayload): TestPayloadBuilder = + createPayloadBuilder(hashPath, miner.elRewardAddress, parentPayload) - def createEcBlockBuilder(hashPath: String, minerRewardL2Address: EthAddress, parent: EcBlock): TestEcBlockBuilder = { - TestEcBlockBuilder(ecClients, elBridgeAddress, elMinerDefaultReward, l2Config.blockDelay, parent = parent).updateBlock( + def createPayloadBuilder(hashPath: String, minerRewardAddress: EthAddress, parentPayload: ExecutionPayload): TestPayloadBuilder = { + TestPayloadBuilder(ecClients, elBridgeAddress, elMinerDefaultReward, l2Config.blockDelay, parentPayload = parentPayload).updatePayload( _.copy( - hash = TestEcBlockBuilder.createBlockHash(hashPath), - minerRewardL2Address = minerRewardL2Address, - prevRandao = ELUpdater.calculateRandao(blockchain.vrf(blockchain.height).get, parent.hash) + hash = TestPayloadBuilder.createBlockHash(hashPath), + feeRecipient = minerRewardAddress, + prevRandao = ELUpdater.calculateRandao(blockchain.vrf(blockchain.height).get, parentPayload.hash) ) ) } diff --git a/src/test/scala/units/TestEcBlockBuilder.scala b/src/test/scala/units/TestEcBlockBuilder.scala deleted file mode 100644 index 52c195a7..00000000 --- a/src/test/scala/units/TestEcBlockBuilder.scala +++ /dev/null @@ -1,66 +0,0 @@ -package units - -import org.web3j.abi.datatypes.generated.Uint256 -import units.client.TestEcClients -import units.client.engine.model.{EcBlock, GetLogsResponseEntry, Withdrawal} -import units.eth.{EthAddress, EthereumConstants, Gwei} - -import java.nio.charset.StandardCharsets -import scala.concurrent.duration.FiniteDuration - -class TestEcBlockBuilder private ( - testEcClients: TestEcClients, - elBridgeAddress: EthAddress, - elMinerDefaultReward: Gwei, - private var block: EcBlock, - parentBlock: EcBlock -) { - def updateBlock(f: EcBlock => EcBlock): TestEcBlockBuilder = { - block = f(block) - this - } - - def rewardPrevMiner(elWithdrawalIndex: Int = 0): TestEcBlockBuilder = rewardMiner(parentBlock.minerRewardL2Address, elWithdrawalIndex) - - def rewardMiner(minerRewardL2Address: EthAddress, elWithdrawalIndex: Int = 0): TestEcBlockBuilder = { - block = block.copy(withdrawals = Vector(Withdrawal(elWithdrawalIndex, minerRewardL2Address, elMinerDefaultReward))) - this - } - - def build(): EcBlock = block - def buildAndSetLogs(logs: List[GetLogsResponseEntry] = Nil): EcBlock = { - testEcClients.setBlockLogs(block.hash, elBridgeAddress, Bridge.ElSentNativeEventTopic, logs) - block - } -} - -object TestEcBlockBuilder { - def apply( - testEcClients: TestEcClients, - elBridgeAddress: EthAddress, - elMinerDefaultReward: Gwei, - blockDelay: FiniteDuration, - parent: EcBlock - ): TestEcBlockBuilder = - new TestEcBlockBuilder( - testEcClients, - elBridgeAddress, - elMinerDefaultReward, - EcBlock( - hash = createBlockHash("???"), - parentHash = parent.hash, - stateRoot = EthereumConstants.EmptyRootHashHex, - height = parent.height + 1, - timestamp = parent.timestamp + blockDelay.toSeconds, - minerRewardL2Address = EthAddress.empty, - baseFeePerGas = Uint256.DEFAULT, - gasLimit = 0, - gasUsed = 0, - prevRandao = EthereumConstants.EmptyPrevRandaoHex, - withdrawals = Vector.empty - ), - parent - ) - - def createBlockHash(path: String): BlockHash = BlockHash(eth.hash(path.getBytes(StandardCharsets.UTF_8))) -} diff --git a/src/test/scala/units/TestPayloadBuilder.scala b/src/test/scala/units/TestPayloadBuilder.scala new file mode 100644 index 00000000..821b7f9f --- /dev/null +++ b/src/test/scala/units/TestPayloadBuilder.scala @@ -0,0 +1,66 @@ +package units + +import org.web3j.abi.datatypes.generated.Uint256 +import units.client.TestEcClients +import units.client.engine.model.{ExecutionPayload, GetLogsResponseEntry, Withdrawal} +import units.eth.{EthAddress, EthereumConstants, Gwei} + +import java.nio.charset.StandardCharsets +import scala.concurrent.duration.FiniteDuration + +class TestPayloadBuilder private ( + testEcClients: TestEcClients, + elBridgeAddress: EthAddress, + elMinerDefaultReward: Gwei, + private var payload: ExecutionPayload, + parentPayload: ExecutionPayload +) { + def updatePayload(f: ExecutionPayload => ExecutionPayload): TestPayloadBuilder = { + payload = f(payload) + this + } + + def rewardPrevMiner(elWithdrawalIndex: Int = 0): TestPayloadBuilder = rewardMiner(parentPayload.feeRecipient, elWithdrawalIndex) + + def rewardMiner(minerRewardAddress: EthAddress, elWithdrawalIndex: Int = 0): TestPayloadBuilder = { + payload = payload.copy(withdrawals = Vector(Withdrawal(elWithdrawalIndex, minerRewardAddress, elMinerDefaultReward))) + this + } + + def build(): ExecutionPayload = payload + def buildAndSetLogs(logs: List[GetLogsResponseEntry] = Nil): ExecutionPayload = { + testEcClients.setBlockLogs(payload.hash, elBridgeAddress, Bridge.ElSentNativeEventTopic, logs) + payload + } +} + +object TestPayloadBuilder { + def apply( + testEcClients: TestEcClients, + elBridgeAddress: EthAddress, + elMinerDefaultReward: Gwei, + blockDelay: FiniteDuration, + parentPayload: ExecutionPayload + ): TestPayloadBuilder = + new TestPayloadBuilder( + testEcClients, + elBridgeAddress, + elMinerDefaultReward, + ExecutionPayload( + hash = createBlockHash("???"), + parentHash = parentPayload.hash, + stateRoot = EthereumConstants.EmptyRootHashHex, + height = parentPayload.height + 1, + timestamp = parentPayload.timestamp + blockDelay.toSeconds, + feeRecipient = EthAddress.empty, + baseFeePerGas = Uint256.DEFAULT, + gasLimit = 0, + gasUsed = 0, + prevRandao = EthereumConstants.EmptyPrevRandaoHex, + withdrawals = Vector.empty + ), + parentPayload + ) + + def createBlockHash(path: String): BlockHash = BlockHash(eth.hash(path.getBytes(StandardCharsets.UTF_8))) +} diff --git a/src/test/scala/units/TestSettings.scala b/src/test/scala/units/TestSettings.scala index 9a9e7116..ec0aeccb 100644 --- a/src/test/scala/units/TestSettings.scala +++ b/src/test/scala/units/TestSettings.scala @@ -1,7 +1,7 @@ package units import com.typesafe.config.ConfigFactory -import com.wavesplatform.account.SeedKeyPair +import com.wavesplatform.account.{Address, SeedKeyPair} import com.wavesplatform.db.WithState.AddrWithBalance import com.wavesplatform.settings.WavesSettings import com.wavesplatform.test.{DomainPresets, NumericExt} @@ -23,11 +23,11 @@ case class TestSettings( } object TestSettings { - val Default = TestSettings() + val Default: TestSettings = TestSettings() private object Waves { - val Default = DomainPresets.TransactionStateSnapshot - val WithMining = Default.copy(config = ConfigFactory.parseString("waves.l2.mining-enable = true").withFallback(Default.config)) + val Default: WavesSettings = DomainPresets.TransactionStateSnapshot + val WithMining: WavesSettings = Default.copy(config = ConfigFactory.parseString("waves.l2.mining-enable = true").withFallback(Default.config)) } } @@ -36,6 +36,6 @@ case class ElMinerSettings( wavesBalance: Long = 20_100.waves, stakingBalance: Long = 50_000_000L ) { - val address = account.toAddress - val elRewardAddress = EthAddress.unsafeFrom(address.toEthAddress) + val address: Address = account.toAddress + val elRewardAddress: EthAddress = EthAddress.unsafeFrom(address.toEthAddress) } diff --git a/src/test/scala/units/client/TestEcClients.scala b/src/test/scala/units/client/TestEcClients.scala index 53ad627b..81adcae0 100644 --- a/src/test/scala/units/client/TestEcClients.scala +++ b/src/test/scala/units/client/TestEcClients.scala @@ -12,43 +12,44 @@ import units.client.engine.model.* import units.client.engine.{EngineApiClient, LoggedEngineApiClient} import units.collections.ListOps.* import units.eth.EthAddress -import units.{BlockHash, JobResult, NetworkL2Block} +import units.network.PayloadMessage +import units.{BlockHash, JobResult} class TestEcClients private ( knownBlocks: Atomic[Map[BlockHash, ChainId]], - chains: Atomic[Map[ChainId, List[EcBlock]]], + chains: Atomic[Map[ChainId, List[ExecutionPayload]]], currChainIdValue: AtomicInt, blockchain: Blockchain ) extends ScorexLogging { - def this(genesis: EcBlock, blockchain: Blockchain) = this( - knownBlocks = Atomic(Map(genesis.hash -> 0)), - chains = Atomic(Map(0 -> List(genesis))), + def this(genesisPayload: ExecutionPayload, blockchain: Blockchain) = this( + knownBlocks = Atomic(Map(genesisPayload.hash -> 0)), + chains = Atomic(Map(0 -> List(genesisPayload))), currChainIdValue = AtomicInt(0), blockchain = blockchain ) private def currChainId: ChainId = currChainIdValue.get() - def addKnown(ecBlock: EcBlock): EcBlock = { - knownBlocks.transform(_.updated(ecBlock.hash, currChainId)) - prependToCurrentChain(ecBlock) - ecBlock + def addKnown(payload: ExecutionPayload): ExecutionPayload = { + knownBlocks.transform(_.updated(payload.hash, currChainId)) + prependToCurrentChain(payload) + payload } - private def prependToCurrentChain(b: EcBlock): Unit = - prependToChain(currChainId, b) + private def prependToCurrentChain(payload: ExecutionPayload): Unit = + prependToChain(currChainId, payload) - private def prependToChain(chainId: ChainId, b: EcBlock): Unit = + private def prependToChain(chainId: ChainId, payload: ExecutionPayload): Unit = chains.transform { chains => - chains.updated(chainId, b :: chains(chainId)) + chains.updated(chainId, payload :: chains(chainId)) } - private def currChain: List[EcBlock] = + private def currChain: List[ExecutionPayload] = chains.get().getOrElse(currChainId, throw new RuntimeException(s"Unknown chain $currChainId")) private val forgingBlocks = Atomic(List.empty[ForgingBlock]) - def willForge(ecBlock: EcBlock): Unit = - forgingBlocks.transform(ForgingBlock(ecBlock) :: _) + def willForge(payload: ExecutionPayload): Unit = + forgingBlocks.transform(ForgingBlock(payload) :: _) private val logs = Atomic(Map.empty[GetLogsRequest, List[GetLogsResponseEntry]]) def setBlockLogs(hash: BlockHash, address: EthAddress, topic: String, blockLogs: List[GetLogsResponseEntry]): Unit = { @@ -64,7 +65,7 @@ class TestEcClients private ( val engineApi = new LoggedEngineApiClient( new EngineApiClient { - override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = { + override def forkChoiceUpdated(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = { knownBlocks.get().get(blockHash) match { case Some(cid) => currChainIdValue.set(cid) @@ -79,7 +80,7 @@ class TestEcClients private ( } }.asRight - override def forkChoiceUpdateWithPayloadId( + override def forkChoiceUpdatedWithPayloadId( lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, unixEpochSeconds: Long, @@ -89,10 +90,10 @@ class TestEcClients private ( ): JobResult[PayloadId] = forgingBlocks .get() - .collectFirst { case fb if fb.testBlock.parentHash == lastBlockHash => fb } match { + .collectFirst { case fb if fb.testPayload.parentHash == lastBlockHash => fb } match { case None => throw new RuntimeException( - s"Can't find a suitable block among: ${forgingBlocks.get().map(_.testBlock.hash).mkString(", ")}. Call willForge" + s"Can't find a suitable block among: ${forgingBlocks.get().map(_.testPayload.hash).mkString(", ")}. Call willForge" ) case Some(fb) => fb.payloadId.asRight @@ -100,44 +101,44 @@ class TestEcClients private ( override def getPayload(payloadId: PayloadId): JobResult[JsObject] = forgingBlocks.transformAndExtract(_.withoutFirst { fb => fb.payloadId == payloadId }) match { - case Some(fb) => TestEcBlocks.toPayload(fb.testBlock, fb.testBlock.prevRandao).asRight + case Some(fb) => TestPayloads.toPayloadJson(fb.testPayload, fb.testPayload.prevRandao).asRight case None => throw new RuntimeException( - s"Can't find payload $payloadId among: ${forgingBlocks.get().map(_.testBlock.hash).mkString(", ")}. Call willForge" + s"Can't find payload $payloadId among: ${forgingBlocks.get().map(_.testPayload.hash).mkString(", ")}. Call willForge" ) } - override def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = { - val newBlock = NetworkL2Block(payload).explicitGet().toEcBlock - knownBlocks.get().get(newBlock.parentHash) match { + override def applyNewPayload(payloadJson: JsObject): JobResult[Option[BlockHash]] = { + val newPayload = PayloadMessage(payloadJson).flatMap(_.payloadInfo).explicitGet().payload + knownBlocks.get().get(newPayload.parentHash) match { case Some(cid) => val chain = chains.get()(cid) - if (newBlock.parentHash == chain.head.hash) { - prependToChain(cid, newBlock) - knownBlocks.transform(_.updated(newBlock.hash, cid)) + if (newPayload.parentHash == chain.head.hash) { + prependToChain(cid, newPayload) + knownBlocks.transform(_.updated(newPayload.hash, cid)) } else { // Rollback - log.debug(s"A rollback using ${newBlock.hash} detected") + log.debug(s"A rollback using ${newPayload.hash} detected") val newCid = currChainIdValue.incrementAndGet() - val newChain = newBlock :: chain.dropWhile(_.hash != newBlock.parentHash) + val newChain = newPayload :: chain.dropWhile(_.hash != newPayload.parentHash) chains.transform(_.updated(newCid, newChain)) - knownBlocks.transform(_.updated(newBlock.hash, newCid)) + knownBlocks.transform(_.updated(newPayload.hash, newCid)) } - case None => throw notImplementedCase(s"Can't find a parent block ${newBlock.parentHash} for ${newBlock.hash}") + case None => throw notImplementedCase(s"Can't find a parent block ${newPayload.parentHash} for ${newPayload.hash}") } - Some(newBlock.hash) + Some(newPayload.hash) }.asRight override def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] = - getBlockByHashJson(hash) + notImplementedMethodJob("getPayloadBodyJsonByHash") - override def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = + override def getBlockByNumber(number: BlockNumber): JobResult[Option[ExecutionPayload]] = number match { case BlockNumber.Latest => currChain.headOption.asRight case BlockNumber.Number(n) => currChain.find(_.height == n).asRight } - override def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = { + override def getBlockByHash(hash: BlockHash): JobResult[Option[ExecutionPayload]] = { for { cid <- knownBlocks.get().get(hash) c <- chains.get().get(cid) @@ -145,18 +146,19 @@ class TestEcClients private ( } yield b }.asRight - override def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = - notImplementedMethodJob("getBlockByHashJson") + override def getBlockJsonByHash(hash: BlockHash): JobResult[Option[JsObject]] = + notImplementedMethodJob("getBlockJsonByHash") - override def getLastExecutionBlock: JobResult[EcBlock] = currChain.head.asRight - - override def blockExists(hash: BlockHash): JobResult[Boolean] = notImplementedMethodJob("blockExists") + override def getLatestBlock: JobResult[ExecutionPayload] = currChain.head.asRight override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = { val request = GetLogsRequest(hash, address, List(topic)) getLogsCalls.transform(_ + hash) logs.get().getOrElse(request, throw notImplementedCase("call setBlockLogs")) }.asRight + + override def getPayloadJsonDataByHash(hash: BlockHash): JobResult[PayloadJsonData] = + notImplementedMethodJob("getPayloadJsonDataByHash") } ) @@ -170,9 +172,9 @@ object TestEcClients { private type ChainId = Int - private case class ForgingBlock(payloadId: String, testBlock: EcBlock) + private case class ForgingBlock(payloadId: String, testPayload: ExecutionPayload) private object ForgingBlock { - def apply(testBlock: EcBlock): ForgingBlock = - new ForgingBlock(testBlock.hash.take(16), testBlock) + def apply(testPayload: ExecutionPayload): ForgingBlock = + new ForgingBlock(testPayload.hash.take(16), testPayload) } } diff --git a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala index 6fa062dc..56cde48b 100644 --- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala +++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala @@ -9,7 +9,7 @@ import com.wavesplatform.lang.v1.compiler.Terms import com.wavesplatform.test.NumericExt import com.wavesplatform.transaction.smart.{InvokeScriptTransaction, SetScriptTransaction} import com.wavesplatform.transaction.{Asset, TxHelpers} -import units.client.L2BlockLike +import units.client.CommonBlockData import units.client.contract.HasConsensusLayerDappTxHelpers.* import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees.chainContract.* import units.eth.{EthAddress, EthereumConstants} @@ -23,11 +23,11 @@ trait HasConsensusLayerDappTxHelpers { object chainContract { def setScript(): SetScriptTransaction = TxHelpers.setScript(chainContractAccount, CompiledChainContract.script, fee = setScriptFee) - def setup(genesisBlock: L2BlockLike, elMinerReward: Long): InvokeScriptTransaction = TxHelpers.invoke( + def setup(genesisBlockData: CommonBlockData, elMinerReward: Long): InvokeScriptTransaction = TxHelpers.invoke( dApp = chainContractAddress, func = "setup".some, args = List( - Terms.CONST_STRING(genesisBlock.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(genesisBlockData.hash.drop(2)).explicitGet(), Terms.CONST_LONG(elMinerReward) ), fee = setupFee @@ -50,7 +50,7 @@ trait HasConsensusLayerDappTxHelpers { def extendMainChain( minerAccount: KeyPair, - block: L2BlockLike, + blockData: CommonBlockData, e2cTransfersRootHashHex: String = EmptyE2CTransfersRootHashHex, lastC2ETransferIndex: Long = -1, vrf: ByteStr = currentHitSource @@ -60,8 +60,8 @@ trait HasConsensusLayerDappTxHelpers { dApp = chainContractAddress, func = "extendMainChain".some, args = List( - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), - Terms.CONST_STRING(block.parentHash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.parentHash.drop(2)).explicitGet(), Terms.CONST_BYTESTR(vrf).explicitGet(), Terms.CONST_STRING(e2cTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(lastC2ETransferIndex) @@ -71,7 +71,7 @@ trait HasConsensusLayerDappTxHelpers { def appendBlock( minerAccount: KeyPair, - block: L2BlockLike, + blockData: CommonBlockData, e2cTransfersRootHashHex: String = EmptyE2CTransfersRootHashHex, lastC2ETransferIndex: Long = -1 ): InvokeScriptTransaction = @@ -80,8 +80,8 @@ trait HasConsensusLayerDappTxHelpers { dApp = chainContractAddress, func = "appendBlock".some, args = List( - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), - Terms.CONST_STRING(block.parentHash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.parentHash.drop(2)).explicitGet(), Terms.CONST_STRING(e2cTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(lastC2ETransferIndex) ), @@ -90,7 +90,7 @@ trait HasConsensusLayerDappTxHelpers { def startAltChain( minerAccount: KeyPair, - block: L2BlockLike, + blockData: CommonBlockData, e2cTransfersRootHashHex: String = EmptyE2CTransfersRootHashHex, lastC2ETransferIndex: Long = -1, vrf: ByteStr = currentHitSource @@ -100,8 +100,8 @@ trait HasConsensusLayerDappTxHelpers { dApp = chainContractAddress, func = "startAltChain".some, args = List( - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), - Terms.CONST_STRING(block.parentHash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.parentHash.drop(2)).explicitGet(), Terms.CONST_BYTESTR(vrf).explicitGet(), Terms.CONST_STRING(e2cTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(lastC2ETransferIndex) @@ -111,7 +111,7 @@ trait HasConsensusLayerDappTxHelpers { def extendAltChain( minerAccount: KeyPair, - block: L2BlockLike, + blockData: CommonBlockData, chainId: Long, e2cTransfersRootHashHex: String = EmptyE2CTransfersRootHashHex, lastC2ETransferIndex: Long = -1, @@ -122,8 +122,8 @@ trait HasConsensusLayerDappTxHelpers { dApp = chainContractAddress, func = "extendAltChain".some, args = List( - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), - Terms.CONST_STRING(block.parentHash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.parentHash.drop(2)).explicitGet(), Terms.CONST_BYTESTR(vrf).explicitGet(), Terms.CONST_LONG(chainId), Terms.CONST_STRING(e2cTransfersRootHashHex.drop(2)).explicitGet(), @@ -165,7 +165,7 @@ trait HasConsensusLayerDappTxHelpers { def withdraw( sender: KeyPair, - block: L2BlockLike, + blockData: CommonBlockData, merkleProof: Seq[Digest], transferIndexInBlock: Int, amount: Long @@ -175,7 +175,7 @@ trait HasConsensusLayerDappTxHelpers { dApp = chainContractAddress, func = "withdraw".some, args = List( - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockData.hash.drop(2)).explicitGet(), Terms.ARR(merkleProof.map[Terms.EVALUATED](x => Terms.CONST_BYTESTR(ByteStr(x)).explicitGet()).toVector, limited = false).explicitGet(), Terms.CONST_LONG(transferIndexInBlock), Terms.CONST_LONG(amount) diff --git a/src/test/scala/units/client/engine/model/TestEcBlocks.scala b/src/test/scala/units/client/engine/model/TestEcBlocks.scala deleted file mode 100644 index 389f9d91..00000000 --- a/src/test/scala/units/client/engine/model/TestEcBlocks.scala +++ /dev/null @@ -1,26 +0,0 @@ -package units.client.engine.model - -import com.wavesplatform.account.SeedKeyPair -import com.wavesplatform.common.utils.EitherExt2 -import play.api.libs.json.{JsObject, Json} -import units.NetworkL2Block -import units.util.HexBytesConverter.toHex - -object TestEcBlocks { - def toNetworkBlock(ecBlock: EcBlock, miner: SeedKeyPair, prevRandao: String): NetworkL2Block = - NetworkL2Block.signed(TestEcBlocks.toPayload(ecBlock, prevRandao), miner.privateKey).explicitGet() - - def toPayload(ecBlock: EcBlock, prevRandao: String): JsObject = Json.obj( - "blockHash" -> ecBlock.hash, - "timestamp" -> toHex(ecBlock.timestamp), - "blockNumber" -> toHex(ecBlock.height), - "parentHash" -> ecBlock.parentHash, - "stateRoot" -> ecBlock.stateRoot, - "feeRecipient" -> ecBlock.minerRewardL2Address, - "prevRandao" -> prevRandao, - "baseFeePerGas" -> toHex(ecBlock.baseFeePerGas), - "gasLimit" -> toHex(ecBlock.gasLimit), - "gasUsed" -> toHex(ecBlock.gasUsed), - "withdrawals" -> ecBlock.withdrawals - ) -} diff --git a/src/test/scala/units/client/engine/model/TestPayloads.scala b/src/test/scala/units/client/engine/model/TestPayloads.scala new file mode 100644 index 00000000..1d66fcc6 --- /dev/null +++ b/src/test/scala/units/client/engine/model/TestPayloads.scala @@ -0,0 +1,20 @@ +package units.client.engine.model + +import play.api.libs.json.{JsObject, Json} +import units.util.HexBytesConverter.toHex + +object TestPayloads { + def toPayloadJson(payload: ExecutionPayload, prevRandao: String): JsObject = Json.obj( + "blockHash" -> payload.hash, + "timestamp" -> toHex(payload.timestamp), + "blockNumber" -> toHex(payload.height), + "parentHash" -> payload.parentHash, + "stateRoot" -> payload.stateRoot, + "feeRecipient" -> payload.feeRecipient, + "prevRandao" -> prevRandao, + "baseFeePerGas" -> toHex(payload.baseFeePerGas), + "gasLimit" -> toHex(payload.gasLimit), + "gasUsed" -> toHex(payload.gasUsed), + "withdrawals" -> payload.withdrawals + ) +} diff --git a/src/test/scala/units/eth/EmptyL2BlockTestSuite.scala b/src/test/scala/units/eth/EmptyPayloadTestSuite.scala similarity index 91% rename from src/test/scala/units/eth/EmptyL2BlockTestSuite.scala rename to src/test/scala/units/eth/EmptyPayloadTestSuite.scala index c965af99..1dc34562 100644 --- a/src/test/scala/units/eth/EmptyL2BlockTestSuite.scala +++ b/src/test/scala/units/eth/EmptyPayloadTestSuite.scala @@ -5,15 +5,15 @@ import org.scalatest.freespec.AnyFreeSpec import org.web3j.abi.datatypes.generated.Uint256 import play.api.libs.json.Json import units.BlockHash -import units.client.engine.model.EcBlock -import units.eth.EmptyL2Block.Params +import units.client.engine.model.ExecutionPayload +import units.eth.EmptyPayload.Params -class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { +class EmptyPayloadTestSuite extends AnyFreeSpec with BaseSuite { private val DefaultFeeRecipient = EthAddress.unsafeFrom("0x283c3c6ad2043af4d4e7d261809260fdab4a62d2") "mkExecutionPayload" in { // Got from eth_getBlockByHash - val parentEcBlockJson = + val parentBlockJson = """{ | "number": "0xf", | "hash": "0x296e3d6de01dbe3c109e13799d6efd2b68469075149ee4e1d8d644765e2cd620", @@ -50,8 +50,8 @@ class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { | "parentBeaconBlockRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" |}""".stripMargin - val parentEcBlock = Json.parse(parentEcBlockJson).as[EcBlock] - EmptyL2Block.mkExecutionPayload(parentEcBlock) shouldBe Json.parse( + val parentPayload = Json.parse(parentBlockJson).as[ExecutionPayload] + EmptyPayload.mkExecutionPayloadJson(parentPayload) shouldBe Json.parse( """{ | "parentHash": "0x296e3d6de01dbe3c109e13799d6efd2b68469075149ee4e1d8d644765e2cd620", | "feeRecipient": "0x0000000000000000000000000000000000000000", @@ -77,7 +77,7 @@ class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { "calculateHash" - { "hash 1" in { val expected = "0xfb0c23d4b394710700e8b4588905cc0a123c088044a6326266280798cb6a5a92" - val actual = EmptyL2Block.calculateHash( + val actual = EmptyPayload.calculateHash( Params( parentHash = BlockHash("0x49a8da0a609fbf1d68535a0766ecb0fe35d9cdac6ba459281daa944fa4c273b9"), parentStateRoot = "0xc0687a72deb7cec7caa72e99a985f3475cb780321d0572a9975fa7638c29c4e1", @@ -93,7 +93,7 @@ class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { "hash 2" in { val expected = "0x86ecb0dc1b4f2a2f90f9cac69831ad9c3fc029e59d900b84b37fbee5e9963275" - val actual = EmptyL2Block.calculateHash( + val actual = EmptyPayload.calculateHash( Params( parentHash = BlockHash("0xfd07fb55cefbec6c0a74aa37058dd0462b71c6ef385ec5388cd2b2092618e895"), parentStateRoot = "0xd254bdd872c29ea362569bc5427843b416efca19a2c8e60af941b694cf48e40d", @@ -110,7 +110,7 @@ class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { "calculateGasFee" - { "0x11ebac8b" in { - EmptyL2Block.calculateGasFee( + EmptyPayload.calculateGasFee( parentGasLimit = 0x1002000, parentBaseFeePerGas = new Uint256(0x147b0e55), 0 @@ -118,7 +118,7 @@ class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { } "0xfae36fa" in { - EmptyL2Block.calculateGasFee( + EmptyPayload.calculateGasFee( parentGasLimit = 0x1002800, parentBaseFeePerGas = new Uint256(0x11ebac8b), 0 @@ -127,7 +127,7 @@ class EmptyL2BlockTestSuite extends AnyFreeSpec with BaseSuite { // TODO test with parentGasUsed != 0 "0x1ac012b7" in { - EmptyL2Block.calculateGasFee( + EmptyPayload.calculateGasFee( parentGasLimit = 0x1001400, parentBaseFeePerGas = new Uint256(0x1e925e88), 0 diff --git a/src/test/scala/units/network/TestBlocksObserver.scala b/src/test/scala/units/network/TestBlocksObserver.scala deleted file mode 100644 index fd60f705..00000000 --- a/src/test/scala/units/network/TestBlocksObserver.scala +++ /dev/null @@ -1,21 +0,0 @@ -package units.network - -import com.wavesplatform.network.ChannelObservable -import com.wavesplatform.utils.ScorexLogging -import io.netty.channel.Channel -import monix.eval.Task -import monix.execution.CancelableFuture -import units.network.BlocksObserverImpl.BlockWithChannel -import units.{BlockHash, NetworkL2Block} - -class TestBlocksObserver(override val getBlockStream: ChannelObservable[NetworkL2Block]) extends BlocksObserver with ScorexLogging { - override def loadBlock(req: BlockHash): CancelableFuture[(Channel, NetworkL2Block)] = { - log.debug(s"loadBlock($req)") - CancelableFuture.never - } - - def requestBlock(req: BlockHash): Task[BlockWithChannel] = { - log.debug(s"requestBlock($req)") - Task.never - } -} diff --git a/src/test/scala/units/network/TestPayloadObserver.scala b/src/test/scala/units/network/TestPayloadObserver.scala new file mode 100644 index 00000000..d9fc1186 --- /dev/null +++ b/src/test/scala/units/network/TestPayloadObserver.scala @@ -0,0 +1,55 @@ +package units.network + +import com.wavesplatform.account.{PrivateKey, PublicKey} +import com.wavesplatform.network.ChannelGroupExt +import com.wavesplatform.utils.ScorexLogging +import io.netty.channel.group.DefaultChannelGroup +import monix.execution.CancelableFuture +import monix.reactive.Observable +import play.api.libs.json.JsObject +import units.eth.EthAddress +import units.{BlockHash, ExecutionPayloadInfo} + +import java.util.concurrent.ConcurrentHashMap + +class TestPayloadObserver(messages: Observable[PayloadMessage], allChannels: DefaultChannelGroup) extends PayloadObserver with ScorexLogging { + + private val lastPayloadMessages: ConcurrentHashMap[BlockHash, PayloadMessage] = + new ConcurrentHashMap[BlockHash, PayloadMessage]() + + override def loadPayload(req: BlockHash): CancelableFuture[ExecutionPayloadInfo] = { + log.debug(s"loadPayload($req)") + CancelableFuture.never + } + + override def broadcastSigned(payloadJson: JsObject, signer: PrivateKey): Either[String, PayloadMessage] = { + log.debug(s"broadcastSigned($payloadJson, $signer)") + val pm = PayloadMessage.signed(payloadJson, signer) + allChannels.broadcast(pm) + pm + } + + override def broadcast(hash: BlockHash): Unit = { + log.debug(s"broadcast($hash)") + Option(lastPayloadMessages.get(hash)).foreach { pm => + allChannels.broadcast(pm) + } + lastPayloadMessages.remove(hash) + } + + override def updateMinerPublicKeys(newKeys: Map[EthAddress, PublicKey]): Unit = { + log.debug(s"updateMinerPublicKeys($newKeys)") + } + + override def getPayloadStream: Observable[ExecutionPayloadInfo] = { + log.debug("getPayloadStream") + messages.concatMapIterable { pm => + pm.payloadInfo match { + case Right(epi) => + lastPayloadMessages.put(pm.hash, pm) + List(epi) + case Left(_) => List.empty + } + } + } +}