/// <summary> /// Refunds are issued as a result of calling specific <see cref="VirtualMachine" /> opcodes like SSTORE or /// SELFDESTRUCT. /// </summary> /// <param name="gasLimit"> /// Gas limit of the entry needed here because /// (<paramref name="gasLimit" /> - <paramref name="unspentGas" />) / 2 is a hard cap for the gas refund. /// </param> /// <param name="unspentGas"> /// Unspent gas of the entry needed here because /// (<paramref name="gasLimit" /> - <paramref name="unspentGas" />) / 2 is a hard cap for the gas refund. /// </param> /// <param name="substate">Substate of the transaction before the refunds are issued.</param> /// <param name="env">Details of the execution environment.</param> /// <param name="spec">Provides the refund logic version details.</param> /// <returns>Returns spent gas after the refund.</returns> private ulong Refund(ulong gasLimit, ulong unspentGas, TransactionSubstate substate, ExecutionEnvironment env, IReleaseSpec spec) { var spentGas = gasLimit; if (substate.IsError) { return(spentGas); } spentGas -= unspentGas; var refund = substate.ShouldRevert ? 0 : Math.Min(spentGas / 2UL, (ulong)(substate.Refund + substate.DestroyList.Count * RefundOf.Destroy)); if (_logger.IsEnabled(LogEventLevel.Verbose)) { _logger.Verbose("Refunding unused gas of {unspent} and refund of {refund}", unspentGas, refund); } var refundValue = (unspentGas + refund) * env.GasPrice; _stateProvider.AddToBalance(env.Sender, refundValue, spec); spentGas -= refund; return(spentGas); }
/// <summary> /// Accounts for which SELF_DESTRUCT opcode was invoked during the tx execution. /// Note that one of them can be coinbase / validator and in such case the miner / validator rewards are lost. /// </summary> /// <param name="substate"></param> private void DestroyAccounts(TransactionSubstate substate) { foreach (var toBeDestroyed in substate.DestroyList) { if (_logger.IsEnabled(LogEventLevel.Verbose)) { _logger.Verbose($"Destroying account {toBeDestroyed}"); } _stateProvider.DeleteAccount(toBeDestroyed); } }
private void DeployCode(ExecutionEnvironment env, TransactionSubstate substate, ref ulong unspentGas, IReleaseSpec spec) { var codeDepositGasCost = (ulong)CodeDepositHandler.CalculateCost(substate.Output.Length, spec); if (unspentGas < codeDepositGasCost && spec.IsEip2Enabled) { throw new OutOfGasException(); } if (unspentGas < codeDepositGasCost) { return; } var codeHash = _stateProvider.UpdateCode(substate.Output); _stateProvider.UpdateCodeHash(env.CodeSource, codeHash, spec); unspentGas -= codeDepositGasCost; }
/// <summary> /// Executes the <paramref name="entry" />. /// </summary> /// <param name="entry">Transaction entry to be executed inside the <see cref="VirtualMachine" />.</param> /// <param name="stateUpdate"><see cref="Delta" /> to be used for execution environment construction</param> /// <param name="txTracer">Tracer to extract the execution steps for debugging or analytics.</param> /// <param name="readOnly">Defines whether the state should be reverted after the execution.</param> /// <exception cref="TransactionCollisionException">Thrown when deployment address already has some code.</exception> /// <exception cref="OutOfGasException">Thrown when not enough gas is available for deposit.</exception> private void Execute(PublicEntry entry, StateUpdate stateUpdate, ITxTracer txTracer, bool readOnly) { var spec = _specProvider.GetSpec(stateUpdate.Number); var(sender, recipient) = ExtractSenderAndRecipient(entry); var isPrecompile = recipient.IsPrecompiled(spec); var env = PrepareEnv(entry, sender, recipient, stateUpdate, isPrecompile); var gasLimit = entry.GasLimit; var intrinsicGas = CalculateIntrinsicGas(entry, spec); if (_logger.IsEnabled(LogEventLevel.Verbose)) { _logger.Verbose("Executing entry {entry}", entry); } if (!ValidateSender(entry, env, txTracer)) { return; } if (!ValidateIntrinsicGas(entry, env, intrinsicGas, txTracer)) { return; } if (!ValidateDeltaGasLimit(entry, env, txTracer)) { return; } if (!_stateProvider.AccountExists(env.Sender)) { if (env.GasPrice == UInt256.Zero) { _stateProvider.CreateAccount(env.Sender, UInt256.Zero); } } if (!ValidateSenderBalance(entry, env, intrinsicGas, txTracer)) { return; } if (!ValidateNonce(entry, env, txTracer)) { return; } InitEntryExecution(env, gasLimit, spec, txTracer); // we prepare two fields to track the amount of gas spent / left var unspentGas = gasLimit - intrinsicGas; var spentGas = gasLimit; // the snapshots are needed to revert the subroutine state changes in case of an VM exception var stateSnapshot = _stateProvider.TakeSnapshot(); var storageSnapshot = _storageProvider.TakeSnapshot(); // we subtract value from sender // it will be added to recipient at the later stage (inside the VM) _stateProvider.SubtractFromBalance(sender, env.Value, spec); // we fail unless we succeed var statusCode = StatusCode.Failure; TransactionSubstate substate = null; try { if (entry.IsValidDeploymentEntry) { PrepareContractAccount(env.CodeSource); } var executionType = entry.IsValidDeploymentEntry ? ExecutionType.Create : ExecutionType.Call; using (var state = new VmState((long)unspentGas, env, executionType, isPrecompile, true, false)) { substate = _virtualMachine.Run(state, txTracer); unspentGas = (ulong)state.GasAvailable; } if (substate.ShouldRevert || substate.IsError) { if (_logger.IsEnabled(LogEventLevel.Verbose)) { _logger.Verbose("Restoring state from before transaction"); } _stateProvider.Restore(stateSnapshot); _storageProvider.Restore(storageSnapshot); } else { if (entry.IsValidDeploymentEntry) { DeployCode(env, substate, ref unspentGas, spec); } DestroyAccounts(substate); statusCode = StatusCode.Success; } spentGas = Refund(gasLimit, unspentGas, substate, env, spec); } catch (Exception ex) when(ex is EvmException || ex is OverflowException) { if (_logger.IsEnabled(LogEventLevel.Verbose)) { _logger.Verbose($"EVM EXCEPTION: {ex.GetType().Name}"); } _stateProvider.Restore(stateSnapshot); _storageProvider.Restore(storageSnapshot); } if (_logger.IsEnabled(LogEventLevel.Verbose)) { _logger.Verbose("Gas spent: " + spentGas); } var gasBeneficiary = stateUpdate.GasBeneficiary; var wasBeneficiaryAccountDestroyed = statusCode != StatusCode.Failure && (substate?.DestroyList.Contains(gasBeneficiary) ?? false); if (!wasBeneficiaryAccountDestroyed) { if (!_stateProvider.AccountExists(gasBeneficiary)) { _stateProvider.CreateAccount(gasBeneficiary, spentGas * env.GasPrice); } else { _stateProvider.AddToBalance(gasBeneficiary, spentGas * env.GasPrice, spec); } } if (!readOnly) { _storageProvider.Commit(txTracer.IsTracingState ? txTracer : null); _stateProvider.Commit(spec, txTracer.IsTracingState ? txTracer : null); stateUpdate.GasUsed += (long)spentGas; } else { _storageProvider.Reset(); _stateProvider.Reset(); } if (txTracer.IsTracingReceipt) { if (statusCode == StatusCode.Failure) { txTracer.MarkAsFailed(env.CodeSource, (long)spentGas, substate?.ShouldRevert ?? false ? substate.Output : Bytes.Empty, substate?.Error); } else { if (substate == null) { throw new InvalidOperationException("Substate should not be null after a successful VM run."); } txTracer.MarkAsSuccess(env.CodeSource, (long)spentGas, substate.Output, substate.Logs.Any() ? substate.Logs.ToArray() : LogEntry.EmptyLogs); } } }
protected void RunTest(VirtualMachineTest test) { TestContext.WriteLine($"Running {test.GetType().FullName}"); VirtualMachine machine = new VirtualMachine(_stateProvider, _storageProvider, _blockhashProvider, _specProvider, _logManager); ExecutionEnvironment environment = new ExecutionEnvironment(); environment.Value = test.Execution.Value; environment.CallDepth = 0; environment.Sender = test.Execution.Caller; environment.ExecutingAccount = test.Execution.Address; BlockHeader header = new BlockHeader( Keccak.OfAnEmptyString, Keccak.OfAnEmptySequenceRlp, test.Environment.CurrentCoinbase, test.Environment.CurrentDifficulty, test.Environment.CurrentNumber, (long)test.Environment.CurrentGasLimit, test.Environment.CurrentTimestamp, Bytes.Empty); environment.CurrentBlock = header; environment.GasPrice = test.Execution.GasPrice; environment.InputData = test.Execution.Data; environment.CodeInfo = new CodeInfo(test.Execution.Code); environment.Originator = test.Execution.Origin; foreach (KeyValuePair <Address, AccountState> accountState in test.Pre) { foreach (KeyValuePair <UInt256, byte[]> storageItem in accountState.Value.Storage) { _storageProvider.Set(new StorageCell(accountState.Key, storageItem.Key), storageItem.Value); if (accountState.Key.Equals(test.Execution.Address)) { _storageProvider.Set(new StorageCell(accountState.Key, storageItem.Key), storageItem.Value); } } _stateProvider.UpdateCode(accountState.Value.Code); _stateProvider.CreateAccount(accountState.Key, accountState.Value.Balance); Keccak codeHash = _stateProvider.UpdateCode(accountState.Value.Code); _stateProvider.UpdateCodeHash(accountState.Key, codeHash, Olympic.Instance); for (int i = 0; i < accountState.Value.Nonce; i++) { _stateProvider.IncrementNonce(accountState.Key); } } EvmState state = new EvmState((long)test.Execution.Gas, environment, ExecutionType.Transaction, true, false); _storageProvider.Commit(); _stateProvider.Commit(Olympic.Instance); TransactionSubstate substate = machine.Run(state, NullTxTracer.Instance); if (test.Out == null) { Assert.NotNull(substate.Error); return; } Assert.True(Bytes.AreEqual(test.Out, substate.Output), $"Exp: {test.Out.ToHexString(true)} != Actual: {substate.Output.ToHexString(true)}"); Assert.AreEqual((long)test.Gas, state.GasAvailable, "gas available"); foreach (KeyValuePair <Address, AccountState> accountState in test.Post) { bool accountExists = _stateProvider.AccountExists(accountState.Key); UInt256 balance = accountExists ? _stateProvider.GetBalance(accountState.Key) : 0; UInt256 nonce = accountExists ? _stateProvider.GetNonce(accountState.Key) : 0; Assert.AreEqual(accountState.Value.Balance, balance, $"{accountState.Key} Balance"); Assert.AreEqual(accountState.Value.Nonce, nonce, $"{accountState.Key} Nonce"); // TODO: not testing properly 0 balance accounts if (accountExists) { byte[] code = _stateProvider.GetCode(accountState.Key); Assert.AreEqual(accountState.Value.Code, code, $"{accountState.Key} Code"); } foreach (KeyValuePair <UInt256, byte[]> storageItem in accountState.Value.Storage) { byte[] value = _storageProvider.Get(new StorageCell(accountState.Key, storageItem.Key)); Assert.True(Bytes.AreEqual(storageItem.Value, value), $"Storage[{accountState.Key}_{storageItem.Key}] Exp: {storageItem.Value.ToHexString(true)} != Actual: {value.ToHexString(true)}"); } } }
private long Refund(long gasLimit, long unspentGas, TransactionSubstate substate, Address sender, in UInt256 gasPrice, IReleaseSpec spec)
private void Execute(Transaction transaction, BlockHeader block, ITxTracer txTracer, ExecutionOptions executionOptions) { bool eip658NotEnabled = !_specProvider.GetSpec(block.Number).IsEip658Enabled; // restore is CallAndRestore - previous call, we will restore state after the execution bool restore = (executionOptions & ExecutionOptions.Restore) == ExecutionOptions.Restore; bool noValidation = (executionOptions & ExecutionOptions.NoValidation) == ExecutionOptions.NoValidation; // commit - is for standard execute, we will commit thee state after execution bool commit = (executionOptions & ExecutionOptions.Commit) == ExecutionOptions.Commit || eip658NotEnabled; //!commit - is for build up during block production, we won't commit state after each transaction to support rollbacks //we commit only after all block is constructed bool notSystemTransaction = !transaction.IsSystem(); bool deleteCallerAccount = false; IReleaseSpec spec = _specProvider.GetSpec(block.Number); if (!notSystemTransaction) { spec = new SystemTransactionReleaseSpec(spec); } UInt256 value = transaction.Value; if (!transaction.TryCalculatePremiumPerGas(block.BaseFeePerGas, out UInt256 premiumPerGas) && !noValidation) { TraceLogInvalidTx(transaction, "MINER_PREMIUM_IS_NEGATIVE"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "miner premium is negative"); return; } UInt256 effectiveGasPrice = transaction.CalculateEffectiveGasPrice(spec.IsEip1559Enabled, block.BaseFeePerGas); long gasLimit = transaction.GasLimit; byte[] machineCode = transaction.IsContractCreation ? transaction.Data : null; byte[] data = transaction.IsMessageCall ? transaction.Data : Array.Empty <byte>(); Address?caller = transaction.SenderAddress; if (_logger.IsTrace) { _logger.Trace($"Executing tx {transaction.Hash}"); } if (caller is null) { TraceLogInvalidTx(transaction, "SENDER_NOT_SPECIFIED"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "sender not specified"); return; } if (!noValidation && _stateProvider.IsInvalidContractSender(spec, caller)) { TraceLogInvalidTx(transaction, "SENDER_IS_CONTRACT"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "sender has deployed code"); return; } long intrinsicGas = IntrinsicGasCalculator.Calculate(transaction, spec); if (_logger.IsTrace) { _logger.Trace($"Intrinsic gas calculated for {transaction.Hash}: " + intrinsicGas); } if (notSystemTransaction) { if (gasLimit < intrinsicGas) { TraceLogInvalidTx(transaction, $"GAS_LIMIT_BELOW_INTRINSIC_GAS {gasLimit} < {intrinsicGas}"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "gas limit below intrinsic gas"); return; } if (!noValidation && gasLimit > block.GasLimit - block.GasUsed) { TraceLogInvalidTx(transaction, $"BLOCK_GAS_LIMIT_EXCEEDED {gasLimit} > {block.GasLimit} - {block.GasUsed}"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "block gas limit exceeded"); return; } } if (!_stateProvider.AccountExists(caller)) { // hacky fix for the potential recovery issue if (transaction.Signature != null) { transaction.SenderAddress = _ecdsa.RecoverAddress(transaction, !spec.ValidateChainId); } if (caller != transaction.SenderAddress) { if (_logger.IsWarn) { _logger.Warn( $"TX recovery issue fixed - tx was coming with sender {caller} and the now it recovers to {transaction.SenderAddress}"); } caller = transaction.SenderAddress; } else { TraceLogInvalidTx(transaction, $"SENDER_ACCOUNT_DOES_NOT_EXIST {caller}"); if (!commit || noValidation || effectiveGasPrice == UInt256.Zero) { deleteCallerAccount = !commit || restore; _stateProvider.CreateAccount(caller, UInt256.Zero); } } if (caller is null) { throw new InvalidDataException( $"Failed to recover sender address on tx {transaction.Hash} when previously recovered sender account did not exist."); } } UInt256 senderReservedGasPayment = noValidation ? UInt256.Zero : (ulong)gasLimit * effectiveGasPrice; if (notSystemTransaction) { UInt256 senderBalance = _stateProvider.GetBalance(caller); if (!noValidation && ((ulong)intrinsicGas * effectiveGasPrice + value > senderBalance || senderReservedGasPayment + value > senderBalance)) { TraceLogInvalidTx(transaction, $"INSUFFICIENT_SENDER_BALANCE: ({caller})_BALANCE = {senderBalance}"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "insufficient sender balance"); return; } if (!noValidation && spec.IsEip1559Enabled && !transaction.IsFree() && senderBalance < (UInt256)transaction.GasLimit * transaction.MaxFeePerGas + value) { TraceLogInvalidTx(transaction, $"INSUFFICIENT_MAX_FEE_PER_GAS_FOR_SENDER_BALANCE: ({caller})_BALANCE = {senderBalance}, MAX_FEE_PER_GAS: {transaction.MaxFeePerGas}"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "insufficient MaxFeePerGas for sender balance"); return; } if (transaction.Nonce != _stateProvider.GetNonce(caller)) { TraceLogInvalidTx(transaction, $"WRONG_TRANSACTION_NONCE: {transaction.Nonce} (expected {_stateProvider.GetNonce(caller)})"); QuickFail(transaction, block, txTracer, eip658NotEnabled, "wrong transaction nonce"); return; } _stateProvider.IncrementNonce(caller); } _stateProvider.SubtractFromBalance(caller, senderReservedGasPayment, spec); if (commit) { _stateProvider.Commit(spec, txTracer.IsTracingState ? txTracer : NullTxTracer.Instance); } long unspentGas = gasLimit - intrinsicGas; long spentGas = gasLimit; Snapshot snapshot = _worldState.TakeSnapshot(); _stateProvider.SubtractFromBalance(caller, value, spec); byte statusCode = StatusCode.Failure; TransactionSubstate substate = null; Address?recipientOrNull = null; try { Address?recipient = transaction.GetRecipient(transaction.IsContractCreation ? _stateProvider.GetNonce(caller) : 0); if (transaction.IsContractCreation) { // if transaction is a contract creation then recipient address is the contract deployment address Address contractAddress = recipient; PrepareAccountForContractDeployment(contractAddress !, spec); } if (recipient == null) { // this transaction is not a contract creation so it should have the recipient known and not null throw new InvalidDataException("Recipient has not been resolved properly before tx execution"); } recipientOrNull = recipient; ExecutionEnvironment env = new(); env.TxExecutionContext = new TxExecutionContext(block, caller, effectiveGasPrice); env.Value = value; env.TransferValue = value; env.Caller = caller; env.CodeSource = recipient; env.ExecutingAccount = recipient; env.InputData = data ?? Array.Empty <byte>(); env.CodeInfo = machineCode == null ? _virtualMachine.GetCachedCodeInfo(_worldState, recipient, spec) : new CodeInfo(machineCode); ExecutionType executionType = transaction.IsContractCreation ? ExecutionType.Create : ExecutionType.Call; using (EvmState state = new(unspentGas, env, executionType, true, snapshot, false)) { if (spec.UseTxAccessLists) { state.WarmUp(transaction.AccessList); // eip-2930 } if (spec.UseHotAndColdStorage) { state.WarmUp(caller); // eip-2929 state.WarmUp(recipient); // eip-2929 } substate = _virtualMachine.Run(state, _worldState, txTracer); unspentGas = state.GasAvailable; if (txTracer.IsTracingAccess) { txTracer.ReportAccess(state.AccessedAddresses, state.AccessedStorageCells); } } if (substate.ShouldRevert || substate.IsError) { if (_logger.IsTrace) { _logger.Trace("Restoring state from before transaction"); } _worldState.Restore(snapshot); } else { // tks: there is similar code fo contract creation from init and from CREATE // this may lead to inconsistencies (however it is tested extensively in blockchain tests) if (transaction.IsContractCreation) { long codeDepositGasCost = CodeDepositHandler.CalculateCost(substate.Output.Length, spec); if (unspentGas < codeDepositGasCost && spec.ChargeForTopLevelCreate) { throw new OutOfGasException(); } if (CodeDepositHandler.CodeIsInvalid(spec, substate.Output)) { throw new InvalidCodeException(); } if (unspentGas >= codeDepositGasCost) { Keccak codeHash = _stateProvider.UpdateCode(substate.Output); _stateProvider.UpdateCodeHash(recipient, codeHash, spec); unspentGas -= codeDepositGasCost; } } foreach (Address toBeDestroyed in substate.DestroyList) { if (_logger.IsTrace) { _logger.Trace($"Destroying account {toBeDestroyed}"); } _storageProvider.ClearStorage(toBeDestroyed); _stateProvider.DeleteAccount(toBeDestroyed); if (txTracer.IsTracingRefunds) { txTracer.ReportRefund(RefundOf.Destroy(spec.IsEip3529Enabled)); } } statusCode = StatusCode.Success; } spentGas = Refund(gasLimit, unspentGas, substate, caller, effectiveGasPrice, spec); } catch (Exception ex) when( ex is EvmException || ex is OverflowException) // TODO: OverflowException? still needed? hope not { if (_logger.IsTrace) { _logger.Trace($"EVM EXCEPTION: {ex.GetType().Name}"); } _worldState.Restore(snapshot); } if (_logger.IsTrace) { _logger.Trace("Gas spent: " + spentGas); } Address gasBeneficiary = block.GasBeneficiary; bool gasBeneficiaryNotDestroyed = substate?.DestroyList.Contains(gasBeneficiary) != true; if (statusCode == StatusCode.Failure || gasBeneficiaryNotDestroyed) { if (notSystemTransaction) { UInt256 fees = (ulong)spentGas * premiumPerGas; if (_stateProvider.AccountExists(gasBeneficiary)) { _stateProvider.AddToBalance(gasBeneficiary, fees, spec); } else { _stateProvider.CreateAccount(gasBeneficiary, fees); } if (!transaction.IsFree() && spec.IsEip1559Enabled && spec.Eip1559FeeCollector is not null) { UInt256 burntFees = (ulong)spentGas * block.BaseFeePerGas; if (!burntFees.IsZero) { if (_stateProvider.AccountExists(spec.Eip1559FeeCollector)) { _stateProvider.AddToBalance(spec.Eip1559FeeCollector, burntFees, spec); } else { _stateProvider.CreateAccount(spec.Eip1559FeeCollector, burntFees); } } } } } if (restore) { _storageProvider.Reset(); _stateProvider.Reset(); if (deleteCallerAccount) { _stateProvider.DeleteAccount(caller); } else { _stateProvider.AddToBalance(caller, senderReservedGasPayment, spec); if (notSystemTransaction) { _stateProvider.DecrementNonce(caller); } _stateProvider.Commit(spec); } } else if (commit) { _storageProvider.Commit(txTracer.IsTracingState ? txTracer : NullStorageTracer.Instance); _stateProvider.Commit(spec, txTracer.IsTracingState ? txTracer : NullStateTracer.Instance); } if (!noValidation && notSystemTransaction) { block.GasUsed += spentGas; } if (txTracer.IsTracingReceipt) { Keccak stateRoot = null; if (eip658NotEnabled) { _stateProvider.RecalculateStateRoot(); stateRoot = _stateProvider.StateRoot; } if (statusCode == StatusCode.Failure) { txTracer.MarkAsFailed(recipientOrNull, spentGas, (substate?.ShouldRevert ?? false) ? substate.Output.ToArray() : Array.Empty <byte>(), substate?.Error, stateRoot); } else { txTracer.MarkAsSuccess(recipientOrNull, spentGas, substate.Output.ToArray(), substate.Logs.Any() ? substate.Logs.ToArray() : Array.Empty <LogEntry>(), stateRoot); } } }