public void calculates_rewards_correctly_for_external_addresses()
        {
            _block.Header.Number = 10;
            _block.Body          = new BlockBody(_block.Body.Transactions, new[]
            {
                Build.A.BlockHeader.WithBeneficiary(TestItem.Addresses[0]).WithNumber(9).TestObject,
                Build.A.BlockHeader.WithBeneficiary(TestItem.Addresses[1]).WithNumber(8).TestObject
            });

            var expected = new BlockReward[]
            {
                new BlockReward(TestItem.AddressA, 1, BlockRewardType.External),
                new BlockReward(TestItem.AddressB, 3, BlockRewardType.External),
                new BlockReward(TestItem.AddressC, 5, BlockRewardType.External),
                new BlockReward(TestItem.AddressD, 8, BlockRewardType.External),
            };

            SetupBlockRewards(new Dictionary <Address, BlockReward[]>()
            {
                { _address10, expected }
            });
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(expected);
        }
        protected override void InitSealEngine()
        {
            if (_api.DbProvider == null)
            {
                throw new StepDependencyException(nameof(_api.DbProvider));
            }
            if (_api.ChainSpec == null)
            {
                throw new StepDependencyException(nameof(_api.ChainSpec));
            }
            if (_api.EthereumEcdsa == null)
            {
                throw new StepDependencyException(nameof(_api.EthereumEcdsa));
            }
            if (_api.BlockTree == null)
            {
                throw new StepDependencyException(nameof(_api.BlockTree));
            }

            _api.ValidatorStore = new ValidatorStore(_api.DbProvider.BlockInfosDb);

            ValidSealerStrategy validSealerStrategy = new ValidSealerStrategy();
            AuRaStepCalculator  auRaStepCalculator  = new AuRaStepCalculator(_api.ChainSpec.AuRa.StepDuration, _api.Timestamper, _api.LogManager);

            _api.SealValidator          = _sealValidator = new AuRaSealValidator(_api.ChainSpec.AuRa, auRaStepCalculator, _api.BlockTree, _api.ValidatorStore, validSealerStrategy, _api.EthereumEcdsa, _api.LogManager);
            _api.RewardCalculatorSource = AuRaRewardCalculator.GetSource(_api.ChainSpec.AuRa, _api.AbiEncoder);
            _api.Sealer         = new AuRaSealer(_api.BlockTree, _api.ValidatorStore, auRaStepCalculator, _api.EngineSigner, validSealerStrategy, _api.LogManager);
            _auRaStepCalculator = auRaStepCalculator;
        }
        public void calculates_rewards_correctly_for_ommers(long blockNumber, long expectedReward)
        {
            _block.Header.Number = blockNumber;
            _block.Body          = new BlockBody(_block.Body.Transactions, new[]
            {
                Build.A.BlockHeader.WithBeneficiary(TestItem.AddressB).WithNumber(blockNumber - 1).TestObject,
                Build.A.BlockHeader.WithBeneficiary(TestItem.AddressD).WithNumber(blockNumber - 2).TestObject
            });

            var expected = new BlockReward[]
            {
                new BlockReward(_block.Beneficiary, expectedReward, BlockRewardType.Block),
                new BlockReward(_block.Body.Ommers[0].Beneficiary, expectedReward, BlockRewardType.Uncle),
                new BlockReward(_block.Body.Ommers[1].Beneficiary, expectedReward, BlockRewardType.Uncle),
            };

            SetupBlockRewards(new Dictionary <Address, BlockReward[]>()
            {
                { _address10, expected }
            });
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(expected);
        }
        protected override void InitSealEngine()
        {
            if (_context.DbProvider == null)
            {
                throw new StepDependencyException(nameof(_context.DbProvider));
            }
            if (_context.ChainSpec == null)
            {
                throw new StepDependencyException(nameof(_context.ChainSpec));
            }
            if (_context.EthereumEcdsa == null)
            {
                throw new StepDependencyException(nameof(_context.EthereumEcdsa));
            }
            if (_context.NodeKey == null)
            {
                throw new StepDependencyException(nameof(_context.NodeKey));
            }

            AbiEncoder abiEncoder = new AbiEncoder();

            _context.ValidatorStore = new ValidatorStore(_context.DbProvider.BlockInfosDb);
            IReadOnlyTransactionProcessorSource readOnlyTransactionProcessorSource = new ReadOnlyTransactionProcessorSource(_context.DbProvider, _context.BlockTree, _context.SpecProvider, _context.LogManager);
            IAuRaValidatorProcessorExtension    validatorProcessorExtension        = new AuRaValidatorProcessorFactory(_context.StateProvider, abiEncoder, _context.TransactionProcessor, readOnlyTransactionProcessorSource, _context.BlockTree, _context.ReceiptStorage, _context.ValidatorStore, _context.LogManager)
                                                                                     .CreateValidatorProcessor(_context.ChainSpec.AuRa.Validators);

            AuRaStepCalculator auRaStepCalculator = new AuRaStepCalculator(_context.ChainSpec.AuRa.StepDuration, _context.Timestamper);

            _context.SealValidator          = new AuRaSealValidator(_context.ChainSpec.AuRa, auRaStepCalculator, _context.ValidatorStore, _context.EthereumEcdsa, _context.LogManager);
            _context.RewardCalculatorSource = AuRaRewardCalculator.GetSource(_context.ChainSpec.AuRa, abiEncoder);
            _context.Sealer = new AuRaSealer(_context.BlockTree, _context.ValidatorStore, auRaStepCalculator, _context.NodeKey.Address, new BasicWallet(_context.NodeKey), new ValidSealerStrategy(), _context.LogManager);
            _context.AuRaBlockProcessorExtension = validatorProcessorExtension;
        }
Exemplo n.º 5
0
        protected override void InitSealEngine()
        {
            if (_context.DbProvider == null)
            {
                throw new StepDependencyException(nameof(_context.DbProvider));
            }
            if (_context.ChainSpec == null)
            {
                throw new StepDependencyException(nameof(_context.ChainSpec));
            }
            if (_context.EthereumEcdsa == null)
            {
                throw new StepDependencyException(nameof(_context.EthereumEcdsa));
            }
            if (_context.NodeKey == null)
            {
                throw new StepDependencyException(nameof(_context.NodeKey));
            }


            _context.ValidatorStore = new ValidatorStore(_context.DbProvider.BlockInfosDb);

            AuRaStepCalculator auRaStepCalculator = new AuRaStepCalculator(_context.ChainSpec.AuRa.StepDuration, _context.Timestamper, _context.LogManager);

            _context.SealValidator          = new AuRaSealValidator(_context.ChainSpec.AuRa, auRaStepCalculator, _context.ValidatorStore, _context.EthereumEcdsa, _context.LogManager);
            _context.RewardCalculatorSource = AuRaRewardCalculator.GetSource(_context.ChainSpec.AuRa, _context.AbiEncoder);
            _context.Sealer = new AuRaSealer(_context.BlockTree, _context.ValidatorStore, auRaStepCalculator, _context.NodeKey.Address, new BasicWallet(_context.NodeKey), new ValidSealerStrategy(), _context.LogManager);
        }
        public void calculates_rewards_correctly_before_contract_transition(long blockNumber, long expectedReward)
        {
            _block.Header.Number = blockNumber;
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(new BlockReward(_block.Beneficiary, expectedReward, BlockRewardType.Block));
        }
        public void calculates_rewards_correctly_for_genesis()
        {
            _block.Header.Number = 0;
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEmpty();
        }
        public void calculates_rewards_correctly_after_contract_transition(long blockNumber, long expectedReward)
        {
            _block.Header.Number = blockNumber;
            var expected = new BlockReward(_block.Beneficiary, expectedReward, BlockRewardType.Block);

            SetupBlockRewards(new Dictionary <Address, BlockReward[]>()
            {
                { _address10, new[] { expected } }
            });
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(expected);
        }
        public void calculates_rewards_correctly_after_subsequent_contract_transitions(long blockNumber, ulong expectedReward, Address address)
        {
            _auraParameters.BlockRewardContractTransitions = new Dictionary <long, Address>()
            {
                { 50, _address50 },
                { 150, _address150 }
            };
            _block.Header.Number = blockNumber;
            var expected = new BlockReward(_block.Beneficiary, expectedReward, BlockRewardType.External);

            SetupBlockRewards(new Dictionary <Address, BlockReward[]>()
            {
                { address, new[] { expected } }
            });
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(expected);
        }
Exemplo n.º 10
0
        public void calculates_rewards_correctly_for_ommers(long blockNumber, long expectedReward)
        {
            _block.Number      = blockNumber;
            _block.Body.Ommers = new[]
            {
                Prepare.A.BlockHeader().WithBeneficiary(Address.FromNumber(777)).WithNumber(blockNumber - 1).TestObject,
                Prepare.A.BlockHeader().WithBeneficiary(Address.FromNumber(888)).WithNumber(blockNumber - 2).TestObject
            };

            var expected = new BlockReward[]
            {
                new BlockReward(_block.Beneficiary, expectedReward, BlockRewardType.Block),
                new BlockReward(_block.Body.Ommers[0].Beneficiary, expectedReward, BlockRewardType.Uncle),
                new BlockReward(_block.Body.Ommers[1].Beneficiary, expectedReward, BlockRewardType.Uncle),
            };

            SetupBlockRewards(expected);
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(expected);
        }
Exemplo n.º 11
0
        public void calculates_rewards_correctly_for_external_addresses()
        {
            _block.Number      = 10;
            _block.Body.Ommers = new[]
            {
                Prepare.A.BlockHeader().WithBeneficiary(Address.FromNumber(777)).WithNumber(9).TestObject,
                Prepare.A.BlockHeader().WithBeneficiary(Address.FromNumber(888)).WithNumber(8).TestObject
            };

            var expected = new BlockReward[]
            {
                new BlockReward(TestItem.AddressA, 1, BlockRewardType.External),
                new BlockReward(TestItem.AddressB, 3, BlockRewardType.External),
                new BlockReward(TestItem.AddressC, 5, BlockRewardType.External),
                new BlockReward(TestItem.AddressD, 8, BlockRewardType.External),
            };

            SetupBlockRewards(expected);
            var calculator = new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
            var result     = calculator.CalculateRewards(_block);

            result.Should().BeEquivalentTo(expected);
        }
Exemplo n.º 12
0
        private static async Task RunBenchmarkBlocks()
        {
            Rlp.RegisterDecoders(typeof(ParityTraceDecoder).Assembly);

            /* logging & instrumentation */
            _logManager = new NLogManager("perfTest.logs.txt", null);
            _logger     = _logManager.GetClassLogger();

            if (_logger.IsInfo)
            {
                _logger.Info("Deleting state DBs");
            }

            DeleteDb(FullStateDbPath);
            DeleteDb(FullCodeDbPath);
            DeleteDb(FullReceiptsDbPath);
            DeleteDb(FullPendingTxsDbPath);
            if (_logger.IsInfo)
            {
                _logger.Info("State DBs deleted");
            }

            /* load spec */
            ChainSpecLoader loader = new ChainSpecLoader(new EthereumJsonSerializer());
            string          path   = Path.Combine(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"chainspec", "ropsten.json"));

            _logger.Info($"Loading ChainSpec from {path}");
            ChainSpec chainSpec = loader.Load(File.ReadAllText(path));

            _logger.Info($"ChainSpec loaded");

            var specProvider = new ChainSpecBasedSpecProvider(chainSpec);
            IRewardCalculator rewardCalculator = new RewardCalculator(specProvider);

            var dbProvider   = new RocksDbProvider(DbBasePath, DbConfig.Default, _logManager, true, true);
            var stateDb      = dbProvider.StateDb;
            var codeDb       = dbProvider.CodeDb;
            var traceDb      = dbProvider.TraceDb;
            var blocksDb     = dbProvider.BlocksDb;
            var headersDb    = dbProvider.HeadersDb;
            var blockInfosDb = dbProvider.BlockInfosDb;
            var receiptsDb   = dbProvider.ReceiptsDb;

            /* state & storage */
            var stateProvider   = new StateProvider(stateDb, codeDb, _logManager);
            var storageProvider = new StorageProvider(stateDb, stateProvider, _logManager);

            var ethereumSigner = new EthereumEcdsa(specProvider, _logManager);

            var transactionPool = new TxPool(
                NullTxStorage.Instance,
                Timestamper.Default,
                ethereumSigner,
                specProvider,
                new TxPoolConfig(),
                stateProvider,
                _logManager);

            var blockInfoRepository = new ChainLevelInfoRepository(blockInfosDb);
            var blockTree           = new UnprocessedBlockTreeWrapper(new BlockTree(blocksDb, headersDb, blockInfosDb, blockInfoRepository, specProvider, transactionPool, _logManager));
            var receiptStorage      = new InMemoryReceiptStorage();

            IBlockDataRecoveryStep recoveryStep = new TxSignaturesRecoveryStep(ethereumSigner, transactionPool, _logManager);

            /* blockchain processing */
            IList <IAdditionalBlockProcessor> blockProcessors = new List <IAdditionalBlockProcessor>();
            var blockhashProvider = new BlockhashProvider(blockTree, LimboLogs.Instance);
            var virtualMachine    = new VirtualMachine(stateProvider, storageProvider, blockhashProvider, specProvider, _logManager);
            var processor         = new TransactionProcessor(specProvider, stateProvider, storageProvider, virtualMachine, _logManager);

            ISealValidator sealValidator;

            if (specProvider.ChainId == RopstenSpecProvider.Instance.ChainId)
            {
                var difficultyCalculator = new DifficultyCalculator(specProvider);
                sealValidator = new EthashSealValidator(_logManager, difficultyCalculator, new Ethash(_logManager));
            }
            else if (chainSpec.SealEngineType == SealEngineType.Clique)
            {
                var snapshotManager = new SnapshotManager(CliqueConfig.Default, blocksDb, blockTree, ethereumSigner, _logManager);
                sealValidator    = new CliqueSealValidator(CliqueConfig.Default, snapshotManager, _logManager);
                rewardCalculator = NoBlockRewards.Instance;
                recoveryStep     = new CompositeDataRecoveryStep(recoveryStep, new AuthorRecoveryStep(snapshotManager));
            }
            else if (chainSpec.SealEngineType == SealEngineType.AuRa)
            {
                var abiEncoder         = new AbiEncoder();
                var validatorProcessor = new AuRaAdditionalBlockProcessorFactory(dbProvider.StateDb, stateProvider, abiEncoder, processor, blockTree, receiptStorage, _logManager)
                                         .CreateValidatorProcessor(chainSpec.AuRa.Validators);

                sealValidator    = new AuRaSealValidator(chainSpec.AuRa, new AuRaStepCalculator(chainSpec.AuRa.StepDuration, new Timestamper()), validatorProcessor, ethereumSigner, _logManager);
                rewardCalculator = new AuRaRewardCalculator(chainSpec.AuRa, abiEncoder, processor);
                blockProcessors.Add(validatorProcessor);
            }
            else
            {
                throw new NotSupportedException();
            }

            /* store & validation */
            var headerValidator      = new HeaderValidator(blockTree, sealValidator, specProvider, _logManager);
            var ommersValidator      = new OmmersValidator(blockTree, headerValidator, _logManager);
            var transactionValidator = new TxValidator(chainSpec.ChainId);
            var blockValidator       = new BlockValidator(transactionValidator, headerValidator, ommersValidator, specProvider, _logManager);

            /* blockchain processing */
            var blockProcessor      = new BlockProcessor(specProvider, blockValidator, rewardCalculator, processor, stateDb, codeDb, traceDb, stateProvider, storageProvider, transactionPool, receiptStorage, _logManager, blockProcessors);
            var blockchainProcessor = new BlockchainProcessor(blockTree, blockProcessor, recoveryStep, _logManager, true, false);

            if (chainSpec.SealEngineType == SealEngineType.AuRa)
            {
                stateProvider.CreateAccount(Address.Zero, UInt256.Zero);
                storageProvider.Commit();
                stateProvider.Commit(Homestead.Instance);
                var finalizationManager = new AuRaBlockFinalizationManager(blockTree, blockInfoRepository, blockProcessor, blockProcessors.OfType <IAuRaValidator>().First(), _logManager);
            }

            foreach ((Address address, ChainSpecAllocation allocation) in chainSpec.Allocations)
            {
                stateProvider.CreateAccount(address, allocation.Balance);
                if (allocation.Code != null)
                {
                    Keccak codeHash = stateProvider.UpdateCode(allocation.Code);
                    stateProvider.UpdateCodeHash(address, codeHash, specProvider.GenesisSpec);
                }

                if (allocation.Constructor != null)
                {
                    Transaction constructorTransaction = new Transaction(true)
                    {
                        SenderAddress = address,
                        Init          = allocation.Constructor,
                        GasLimit      = chainSpec.Genesis.GasLimit
                    };

                    processor.Execute(constructorTransaction, chainSpec.Genesis.Header, NullTxTracer.Instance);
                }
            }

            _logger.Info($"Allocations configured, committing...");

            stateProvider.Commit(specProvider.GenesisSpec);

            _logger.Info($"Finalizing genesis...");
            chainSpec.Genesis.Header.StateRoot = stateProvider.StateRoot;
            chainSpec.Genesis.Header.Hash      = BlockHeader.CalculateHash(chainSpec.Genesis.Header);

            if (chainSpec.Genesis.Hash != blockTree.Genesis.Hash)
            {
                throw new Exception("Unexpected genesis hash");
            }

            _logger.Info($"Starting benchmark processor...");
            /* start processing */
            BigInteger totalGas  = BigInteger.Zero;
            Stopwatch  stopwatch = new Stopwatch();
            Block      currentHead;
            long       maxMemory = 0;

            blockTree.NewHeadBlock += (sender, args) =>
            {
                currentHead = args.Block;
                if (currentHead.Number == 0)
                {
                    return;
                }

                maxMemory = Math.Max(maxMemory, GC.GetTotalMemory(false));
                totalGas += currentHead.GasUsed;
                if ((BigInteger)args.Block.Number % 10000 == 9999)
                {
                    stopwatch.Stop();
                    long       ms     = 1_000L * stopwatch.ElapsedTicks / Stopwatch.Frequency;
                    BigInteger number = args.Block.Number + 1;
                    _logger.Warn($"TOTAL after {number} (ms)       : " + ms);
                    _logger.Warn($"TOTAL after {number} blocks/s   : {(decimal) currentHead.Number / (ms / 1000m),5}");
                    _logger.Warn($"TOTAL after {number} Mgas/s     : {((decimal) totalGas / 1000000) / (ms / 1000m),5}");
                    _logger.Warn($"TOTAL after {number} max mem    : {maxMemory}");
                    _logger.Warn($"TOTAL after {number} GC (0/1/2) : {GC.CollectionCount(0)}/{GC.CollectionCount(1)}/{GC.CollectionCount(2)}");
                    _logger.Warn($"Is server GC {number}           : {System.Runtime.GCSettings.IsServerGC}");
                    _logger.Warn($"GC latency mode {number}        : {System.Runtime.GCSettings.LatencyMode}");

                    _logger.Warn($"TOTAL after {number} blocks DB reads      : {Store.Metrics.BlocksDbReads}");
                    _logger.Warn($"TOTAL after {number} blocks DB writes     : {Store.Metrics.BlocksDbWrites}");
                    _logger.Warn($"TOTAL after {number} infos DB reads       : {Store.Metrics.BlockInfosDbReads}");
                    _logger.Warn($"TOTAL after {number} infos DB writes      : {Store.Metrics.BlockInfosDbWrites}");
                    _logger.Warn($"TOTAL after {number} state tree reads     : {Store.Metrics.StateTreeReads}");
                    _logger.Warn($"TOTAL after {number} state tree writes    : {Store.Metrics.StateTreeWrites}");
                    _logger.Warn($"TOTAL after {number} state DB reads       : {Store.Metrics.StateDbReads}");
                    _logger.Warn($"TOTAL after {number} state DB writes      : {Store.Metrics.StateDbWrites}");
                    _logger.Warn($"TOTAL after {number} storage tree reads   : {Store.Metrics.StorageTreeReads}");
                    _logger.Warn($"TOTAL after {number} storage tree writes  : {Store.Metrics.StorageTreeWrites}");
                    _logger.Warn($"TOTAL after {number} tree node hash       : {Store.Metrics.TreeNodeHashCalculations}");
                    _logger.Warn($"TOTAL after {number} tree node RLP decode : {Store.Metrics.TreeNodeRlpDecodings}");
                    _logger.Warn($"TOTAL after {number} tree node RLP encode : {Store.Metrics.TreeNodeRlpEncodings}");
                    _logger.Warn($"TOTAL after {number} code DB reads        : {Store.Metrics.CodeDbReads}");
                    _logger.Warn($"TOTAL after {number} code DB writes       : {Store.Metrics.CodeDbWrites}");
                    _logger.Warn($"TOTAL after {number} receipts DB reads    : {Store.Metrics.ReceiptsDbReads}");
                    _logger.Warn($"TOTAL after {number} receipts DB writes   : {Store.Metrics.ReceiptsDbWrites}");
                    _logger.Warn($"TOTAL after {number} other DB reads       : {Store.Metrics.OtherDbReads}");
                    _logger.Warn($"TOTAL after {number} other DB writes      : {Store.Metrics.OtherDbWrites}");
                    _logger.Warn($"TOTAL after {number} EVM exceptions       : {Evm.Metrics.EvmExceptions}");
                    _logger.Warn($"TOTAL after {number} SLOAD opcodes        : {Evm.Metrics.SloadOpcode}");
                    _logger.Warn($"TOTAL after {number} SSTORE opcodes       : {Evm.Metrics.SstoreOpcode}");
                    _logger.Warn($"TOTAL after {number} EXP opcodes          : {Evm.Metrics.ModExpOpcode}");
                    _logger.Warn($"TOTAL after {number} BLOCKHASH opcodes    : {Evm.Metrics.BlockhashOpcode}");
                    _logger.Warn($"TOTAL after {number} EVM calls            : {Evm.Metrics.Calls}");
                    _logger.Warn($"TOTAL after {number} RIPEMD Precompiles   : {Evm.Metrics.Ripemd160Precompile}");
                    _logger.Warn($"TOTAL after {number} SHA256 Precompiles   : {Evm.Metrics.Sha256Precompile}");
                    // disk space
                    stopwatch.Start();
                }
            };

            bool isStarted = false;

            TaskCompletionSource <object> completionSource = new TaskCompletionSource <object>();

            blockTree.NewBestSuggestedBlock += (sender, args) =>
            {
                if (!isStarted)
                {
                    blockchainProcessor.Process(blockTree.FindBlock(blockTree.Genesis.Hash, BlockTreeLookupOptions.RequireCanonical), ProcessingOptions.None, NullBlockTracer.Instance);
                    stopwatch.Start();
                    blockchainProcessor.Start();
                    isStarted = true;
                }

                if (args.Block.Number == BlocksToLoad)
                {
                    completionSource.SetResult(null);
                }
            };

            await Task.WhenAny(completionSource.Task, blockTree.LoadBlocksFromDb(CancellationToken.None, 0, 10000, BlocksToLoad));

            await blockchainProcessor.StopAsync(true).ContinueWith(
                t =>
            {
                if (t.IsFaulted)
                {
                    _logger.Error("processing failed", t.Exception);
                    _logger.Error("inner", t.Exception.InnerException);
                    Console.ReadLine();
                }

                _logger.Info("Block processing completed.");
            });

            stopwatch.Stop();
            Console.ReadLine();
        }