public async Task newPayloadV1_can_insert_blocks_from_cache_when_syncing() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateEngineModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; ExecutionPayloadV1 parentBlockRequest = new(Build.A.Block.WithNumber(2).TestObject); ExecutionPayloadV1[] requests = CreateBlockRequestBranch(parentBlockRequest, Address.Zero, 7); ResultWrapper <PayloadStatusV1> payloadStatus; foreach (ExecutionPayloadV1 r in requests) { payloadStatus = await rpc.engine_newPayloadV1(r); payloadStatus.Data.Status.Should().Be(nameof(PayloadStatusV1.Syncing).ToUpper()); chain.BeaconSync.IsBeaconSyncHeadersFinished().Should().BeTrue(); chain.BeaconSync.ShouldBeInBeaconHeaders().Should().BeFalse(); chain.BeaconPivot.BeaconPivotExists().Should().BeFalse(); } int pivotNum = 3; requests[pivotNum].TryGetBlock(out Block? pivotBlock); // initiate sync ForkchoiceStateV1 forkchoiceStateV1 = new(pivotBlock !.Hash, startingHead, startingHead); ResultWrapper <ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1); forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should() .Be(nameof(PayloadStatusV1.Syncing).ToUpper()); // trigger insertion of blocks in cache into block tree payloadStatus = await rpc.engine_newPayloadV1(requests[^ 1]);
public virtual async Task forkchoiceUpdatedV1_should_communicate_with_boost_relay_through_http() { MergeConfig mergeConfig = new() { Enabled = true, SecondsPerSlot = 1, TerminalTotalDifficulty = "0" }; using MergeTestBlockchain chain = await CreateBlockChain(mergeConfig); IJsonSerializer serializer = chain.JsonSerializer; UInt256 timestamp = Timestamper.UnixTime.Seconds; PayloadAttributes payloadAttributes = new() { Timestamp = timestamp, SuggestedFeeRecipient = Address.Zero, PrevRandao = Keccak.Zero }; string relayUrl = "http://localhost"; MockHttpMessageHandler mockHttp = new(); mockHttp.Expect(HttpMethod.Post, relayUrl + BoostRelay.GetPayloadAttributesPath) .WithContent("{\"timestamp\":\"0x3e8\",\"prevRandao\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"suggestedFeeRecipient\":\"0x0000000000000000000000000000000000000000\"}") .Respond("application/json", "{\"timestamp\":\"0x3e9\",\"prevRandao\":\"0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760\",\"suggestedFeeRecipient\":\"0xb7705ae4c6f81b66cdb323c65f4e8133690fc099\"}"); mockHttp.Expect(HttpMethod.Post, relayUrl + BoostRelay.SendPayloadPath) .WithContent("{\"block\":{\"parentHash\":\"0x1c53bdbf457025f80c6971a9cf50986974eed02f0a9acaeeb49cafef10efd133\",\"feeRecipient\":\"0xb7705ae4c6f81b66cdb323c65f4e8133690fc099\",\"stateRoot\":\"0x1ef7300d8961797263939a3d29bbba4ccf1702fabf02d8ad7a20b454edb6fd2f\",\"receiptsRoot\":\"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421\",\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"prevRandao\":\"0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760\",\"blockNumber\":\"0x1\",\"gasLimit\":\"0x3d0900\",\"gasUsed\":\"0x0\",\"timestamp\":\"0x3e9\",\"extraData\":\"0x\",\"baseFeePerGas\":\"0x0\",\"blockHash\":\"0xb519d89363b891216dbf9bd046f226cae9348a90ba0db00e7ade437fff7a1510\",\"transactions\":[]},\"profit\":\"0x0\"}"); DefaultHttpClient defaultHttpClient = new(mockHttp.ToHttpClient(), serializer, chain.LogManager, 1, 100); BoostRelay boostRelay = new(defaultHttpClient, relayUrl); BoostBlockImprovementContextFactory improvementContextFactory = new(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5000), boostRelay, chain.StateReader); TimeSpan timePerSlot = TimeSpan.FromSeconds(1000); chain.PayloadPreparationService = new PayloadPreparationService( chain.PostMergeBlockProducer !, improvementContextFactory, TimerFactory.Default, chain.LogManager, timePerSlot); IEngineRpcModule rpc = CreateEngineModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; ManualResetEvent wait = new(false); chain.PayloadPreparationService.BlockImproved += (_, _) => wait.Set(); string payloadId = rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(startingHead, Keccak.Zero, startingHead), payloadAttributes).Result.Data .PayloadId !; await wait.WaitOneAsync(100, CancellationToken.None); ResultWrapper <ExecutionPayloadV1?> response = await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId)); ExecutionPayloadV1 executionPayloadV1 = response.Data !; executionPayloadV1.FeeRecipient.Should().Be(TestItem.AddressA); executionPayloadV1.PrevRandao.Should().Be(TestItem.KeccakA); mockHttp.VerifyNoOutstandingExpectation(); } [Test]
public async Task setHead_to_unknown_block_fails() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); ResultWrapper <Result> setHeadResult = await rpc.engine_setHead(TestItem.KeccakF); setHeadResult.Data.Success.Should().BeFalse(); }
private void AssertExecutionStatusChanged(IEngineRpcModule rpc, Keccak headBlockHash, Keccak finalizedBlockHash, Keccak safeBlockHash) { ExecutionStatusResult?result = rpc.engine_executionStatus().Data; Assert.AreEqual(headBlockHash, result.HeadBlockHash); Assert.AreEqual(finalizedBlockHash, result.FinalizedBlockHash); Assert.AreEqual(safeBlockHash, result.SafeBlockHash); }
public async Task forkChoiceUpdatedV1_unknown_block_initiates_syncing() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateEngineModule(chain); Keccak? startingHead = chain.BlockTree.HeadHash; BlockHeader parent = Build.A.BlockHeader .WithNumber(1) .WithHash(TestItem.KeccakA) .WithNonce(0) .WithDifficulty(0) .TestObject; Block block = Build.A.Block .WithNumber(2) .WithParent(parent) .WithNonce(0) .WithDifficulty(0) .WithAuthor(Address.Zero) .WithPostMergeFlag(true) .TestObject; await rpc.engine_newPayloadV1(new ExecutionPayloadV1(block)); // sync has not started yet chain.BeaconSync.IsBeaconSyncHeadersFinished().Should().BeTrue(); chain.BeaconSync.IsBeaconSyncFinished(block.Header).Should().BeTrue(); chain.BeaconSync.ShouldBeInBeaconHeaders().Should().BeFalse(); chain.BeaconPivot.BeaconPivotExists().Should().BeFalse(); BlockTreePointers pointers = new() { BestKnownNumber = 0, BestSuggestedHeader = chain.BlockTree.Genesis !, BestSuggestedBody = chain.BlockTree.FindBlock(0) !, BestKnownBeaconBlock = 0, LowestInsertedHeader = null, LowestInsertedBeaconHeader = null }; AssertBlockTreePointers(chain.BlockTree, pointers); ForkchoiceStateV1 forkchoiceStateV1 = new(block.Hash !, startingHead, startingHead); ResultWrapper <ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1); forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should() .Be(nameof(PayloadStatusV1.Syncing).ToUpper()); chain.BeaconSync.ShouldBeInBeaconHeaders().Should().BeTrue(); chain.BeaconSync.IsBeaconSyncHeadersFinished().Should().BeFalse(); chain.BeaconSync.IsBeaconSyncFinished(chain.BlockTree.FindBlock(block.Hash)?.Header).Should().BeFalse(); AssertBeaconPivotValues(chain.BeaconPivot, block.Header); pointers.LowestInsertedBeaconHeader = block.Header; pointers.BestKnownBeaconBlock = block.Number; pointers.LowestInsertedHeader = block.Header; AssertBlockTreePointers(chain.BlockTree, pointers); AssertExecutionStatusNotChangedV1(rpc, block.Hash !, startingHead, startingHead); }
public async Task assembleBlock_should_not_create_block_with_unknown_parent() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Keccak notExistingHash = TestItem.KeccakH; AssembleBlockRequest assembleBlockRequest = new() { ParentHash = notExistingHash }; ResultWrapper <BlockRequestResult?> response = await rpc.engine_assembleBlock(assembleBlockRequest); response.Data.Should().BeNull(); }
public async Task finaliseBlock_should_succeed() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Block block = Build.A.Block.WithParent(chain.BlockTree.Head !).TestObject; chain.BlockTree.SuggestBlock(block); ResultWrapper <Result> resultWrapper = await rpc.engine_finaliseBlock(block.Hash !); resultWrapper.Data.Should().Be(Result.Ok); }
public async Task can_progress_chain_one_by_one(int count) { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Keccak lastHash = (await ProduceBranch(rpc, chain.BlockTree, count, chain.BlockTree.HeadHash, true)).Last().BlockHash; chain.BlockTree.HeadHash.Should().Be(lastHash); Block?last = RunForAllBlocksInBranch(chain.BlockTree, chain.BlockTree.HeadHash, b => b.IsGenesis, true); last.Should().NotBeNull(); last !.IsGenesis.Should().BeTrue(); }
public async Task newBlock_accepts_first_block() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); BlockRequestResult blockRequestResult = CreateBlockRequest( CreateParentBlockRequestOnHead(chain.BlockTree), TestItem.AddressD); ResultWrapper <NewBlockResult> resultWrapper = await rpc.engine_newBlock(blockRequestResult); resultWrapper.Data.Valid.Should().BeTrue(); new BlockRequestResult(chain.BlockTree.BestSuggestedBody).Should().BeEquivalentTo(blockRequestResult); }
public async Task should_return_invalid_lvh_null_on_invalid_blocks_during_the_sync() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateEngineModule(chain); Keccak? startingHead = chain.BlockTree.HeadHash; BlockHeader parent = Build.A.BlockHeader .WithNumber(1) .WithHash(TestItem.KeccakA) .WithNonce(0) .WithDifficulty(0) .TestObject; Block block = Build.A.Block .WithNumber(2) .WithParent(parent) .WithNonce(0) .WithDifficulty(0) .WithAuthor(Address.Zero) .WithPostMergeFlag(true) .TestObject; ExecutionPayloadV1 startingNewPayload = new(block); await rpc.engine_newPayloadV1(startingNewPayload); ForkchoiceStateV1 forkchoiceStateV1 = new(block.Hash !, startingHead, startingHead); ResultWrapper <ForkchoiceUpdatedV1Result> forkchoiceUpdatedResult = await rpc.engine_forkchoiceUpdatedV1(forkchoiceStateV1); forkchoiceUpdatedResult.Data.PayloadStatus.Status.Should() .Be(nameof(PayloadStatusV1.Syncing).ToUpper()); ExecutionPayloadV1[] requests = CreateBlockRequestBranch(startingNewPayload, TestItem.AddressD, 1); foreach (ExecutionPayloadV1 r in requests) { ResultWrapper <PayloadStatusV1> payloadStatus = await rpc.engine_newPayloadV1(r); payloadStatus.Data.Status.Should().Be(nameof(PayloadStatusV1.Syncing).ToUpper()); } ExecutionPayloadV1[] invalidRequests = CreateBlockRequestBranch(requests[0], TestItem.AddressD, 1); foreach (ExecutionPayloadV1 r in invalidRequests) { r.TryGetBlock(out Block? newBlock); newBlock !.Header.GasLimit = long.MaxValue; // incorrect gas limit newBlock.Header.Hash = newBlock.CalculateHash(); ResultWrapper <PayloadStatusV1> payloadStatus = await rpc.engine_newPayloadV1(new ExecutionPayloadV1(newBlock)); payloadStatus.Data.Status.Should().Be(nameof(PayloadStatusV1.Invalid).ToUpper()); payloadStatus.Data.LatestValidHash.Should().BeNull(); } }
public async Task newBlock_accepts_already_known_block() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Block block = Build.A.Block.WithNumber(1).WithParent(chain.BlockTree.Head !).TestObject; block.Header.Hash = new Keccak("0xdc3419cbd81455372f3e576f930560b35ec828cd6cdfbd4958499e43c68effdf"); chain.BlockTree.SuggestBlock(block); ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(new BlockRequestResult(block)); newBlockResult.Data.Valid.Should().BeTrue(); }
public async Task setHead_no_common_branch_fails() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); BlockHeader parent = Build.A.BlockHeader.WithNumber(1).WithHash(TestItem.KeccakA).TestObject; Block block = Build.A.Block.WithNumber(2).WithParent(parent).TestObject; chain.BlockTree.SuggestBlock(block); ResultWrapper <Result> setHeadResult = await rpc.engine_setHead(block.Hash !); setHeadResult.Data.Success.Should().BeFalse(); }
public async Task forkchoiceUpdatedV1_should_ignore_gas_limit([Values(false, true)] bool relay) { MergeConfig mergeConfig = new() { Enabled = true, SecondsPerSlot = 1, TerminalTotalDifficulty = "0" }; using MergeTestBlockchain chain = await CreateBlockChain(mergeConfig); IBlockImprovementContextFactory improvementContextFactory; if (relay) { IBoostRelay boostRelay = Substitute.For <IBoostRelay>(); boostRelay.GetPayloadAttributes(Arg.Any <PayloadAttributes>(), Arg.Any <CancellationToken>()) .Returns(c => c.Arg <PayloadAttributes>()); improvementContextFactory = new BoostBlockImprovementContextFactory(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5), boostRelay, chain.StateReader); } else { improvementContextFactory = new BlockImprovementContextFactory(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5)); } TimeSpan timePerSlot = TimeSpan.FromSeconds(10); chain.PayloadPreparationService = new PayloadPreparationService( chain.PostMergeBlockProducer !, improvementContextFactory, TimerFactory.Default, chain.LogManager, timePerSlot); IEngineRpcModule rpc = CreateEngineModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; UInt256 timestamp = Timestamper.UnixTime.Seconds; Keccak random = Keccak.Zero; Address feeRecipient = Address.Zero; string payloadId = rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(startingHead, Keccak.Zero, startingHead), new PayloadAttributes { Timestamp = timestamp, SuggestedFeeRecipient = feeRecipient, PrevRandao = random, GasLimit = 10_000_000L }).Result.Data .PayloadId !; ResultWrapper <ExecutionPayloadV1?> response = await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId)); ExecutionPayloadV1 executionPayloadV1 = response.Data !; executionPayloadV1.GasLimit.Should().Be(4_000_000L); }
public async Task setHead_can_reorganize_to_any_block() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); async Task CanReorganizeToBlock(BlockRequestResult block, MergeTestBlockchain testChain) { ResultWrapper <Result> result = await rpc.engine_setHead(block.BlockHash); result.Data.Should().Be(Result.Ok); testChain.BlockTree.HeadHash.Should().Be(block.BlockHash); testChain.BlockTree.Head !.Number.Should().Be(block.Number); testChain.State.StateRoot.Should().Be(testChain.BlockTree.Head !.StateRoot !); } async Task CanReorganizeToAnyBlock(MergeTestBlockchain testChain, params IReadOnlyList <BlockRequestResult>[] branches)
public async Task newBlock_rejects_incorrect_input(Action <BlockRequestResult> breakerAction) { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); BlockRequestResult assembleBlockResult = await GetAssembleBlockResult(chain, rpc); Keccak blockHash = assembleBlockResult.BlockHash; breakerAction(assembleBlockResult); if (blockHash == assembleBlockResult.BlockHash && TryCalculateHash(assembleBlockResult, out var hash)) { assembleBlockResult.BlockHash = hash; } ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(assembleBlockResult); newBlockResult.Data.Valid.Should().BeFalse(); }
public async Task assembleBlock_should_create_block_on_top_of_genesis() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; UInt256 timestamp = Timestamper.UnixTime.Seconds; AssembleBlockRequest assembleBlockRequest = new() { ParentHash = startingHead, Timestamp = timestamp }; ResultWrapper <BlockRequestResult?> response = await rpc.engine_assembleBlock(assembleBlockRequest); BlockRequestResult expected = CreateParentBlockRequestOnHead(chain.BlockTree); expected.GasLimit = 4000000L; expected.BlockHash = new Keccak("0xfe37027d377e75ffb161f11733d8880083378fe6236270c7a2ee1fc7efe71cfd"); expected.LogsBloom = Bloom.Empty; expected.Miner = chain.MinerAddress; expected.Number = 1; expected.ParentHash = startingHead; expected.SetTransactions(Array.Empty <Transaction>()); expected.Timestamp = timestamp; response.Data.Should().BeEquivalentTo(expected); }
public async Task setHead_should_change_head() { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; BlockRequestResult blockRequestResult = CreateBlockRequest( CreateParentBlockRequestOnHead(chain.BlockTree), TestItem.AddressD); ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(blockRequestResult); newBlockResult.Data.Valid.Should().BeTrue(); Keccak newHeadHash = blockRequestResult.BlockHash; ResultWrapper <Result> setHeadResult = await rpc.engine_setHead(newHeadHash !); setHeadResult.Data.Should().Be(Result.Ok); Keccak actualHead = chain.BlockTree.HeadHash; actualHead.Should().NotBe(startingHead); actualHead.Should().Be(newHeadHash); }
public async Task newBlock_accepts_previously_assembled_block_multiple_times([Values(1, 3)] int times) { using MergeTestBlockchain chain = await CreateBlockChain(); IEngineRpcModule rpc = CreateConsensusModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; BlockHeader startingBestSuggestedHeader = chain.BlockTree.BestSuggestedHeader !; AssembleBlockRequest assembleBlockRequest = new() { ParentHash = startingHead }; ResultWrapper <BlockRequestResult?> assembleBlockResult = await rpc.engine_assembleBlock(assembleBlockRequest); assembleBlockResult.Data !.ParentHash.Should().Be(startingHead); for (int i = 0; i < times; i++) { ResultWrapper <NewBlockResult> newBlockResult = await rpc.engine_newBlock(assembleBlockResult.Data !); newBlockResult.Data.Valid.Should().BeTrue(); } Keccak bestSuggestedHeaderHash = chain.BlockTree.BestSuggestedHeader !.Hash !; bestSuggestedHeaderHash.Should().Be(assembleBlockResult.Data !.BlockHash); bestSuggestedHeaderHash.Should().NotBe(startingBestSuggestedHeader !.Hash !); }
public async Task forkchoiceUpdatedV1_should_communicate_with_boost_relay() { MergeConfig mergeConfig = new() { Enabled = true, SecondsPerSlot = 1, TerminalTotalDifficulty = "0" }; using MergeTestBlockchain chain = await CreateBlockChain(mergeConfig); IBoostRelay boostRelay = Substitute.For <IBoostRelay>(); boostRelay.GetPayloadAttributes(Arg.Any <PayloadAttributes>(), Arg.Any <CancellationToken>()) .Returns(c => { PayloadAttributes payloadAttributes = c.Arg <PayloadAttributes>(); payloadAttributes.SuggestedFeeRecipient = TestItem.AddressA; payloadAttributes.PrevRandao = TestItem.KeccakA; payloadAttributes.Timestamp += 1; payloadAttributes.GasLimit = 10_000_000L; return(payloadAttributes); }); BoostBlockImprovementContextFactory improvementContextFactory = new(chain.BlockProductionTrigger, TimeSpan.FromSeconds(5), boostRelay, chain.StateReader); TimeSpan timePerSlot = TimeSpan.FromSeconds(10); chain.PayloadPreparationService = new PayloadPreparationService( chain.PostMergeBlockProducer !, improvementContextFactory, TimerFactory.Default, chain.LogManager, timePerSlot); IEngineRpcModule rpc = CreateEngineModule(chain); Keccak startingHead = chain.BlockTree.HeadHash; UInt256 timestamp = Timestamper.UnixTime.Seconds; Keccak random = Keccak.Zero; Address feeRecipient = Address.Zero; BoostExecutionPayloadV1?sentItem = null; boostRelay.When(b => b.SendPayload(Arg.Any <BoostExecutionPayloadV1>(), Arg.Any <CancellationToken>())) .Do(c => sentItem = c.Arg <BoostExecutionPayloadV1>()); ManualResetEvent wait = new(false); chain.PayloadPreparationService.BlockImproved += (_, _) => wait.Set(); string payloadId = rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(startingHead, Keccak.Zero, startingHead), new PayloadAttributes { Timestamp = timestamp, SuggestedFeeRecipient = feeRecipient, PrevRandao = random }).Result.Data .PayloadId !; await wait.WaitOneAsync(100, CancellationToken.None); ResultWrapper <ExecutionPayloadV1?> response = await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId)); ExecutionPayloadV1 executionPayloadV1 = response.Data !; executionPayloadV1.FeeRecipient.Should().Be(TestItem.AddressA); executionPayloadV1.PrevRandao.Should().Be(TestItem.KeccakA); executionPayloadV1.GasLimit.Should().Be(10_000_000L); executionPayloadV1.Should().BeEquivalentTo(sentItem !.Block); sentItem.Profit.Should().Be(0); }