Etaifang Source Code Analysis-Specific Procedures for Transaction Enforcement and Contract Creation

Posted by jdesilva on Tue, 10 Sep 2019 08:28:20 +0200

1. The StateProcessor processor traverses every transaction in the block and executes the transaction through the ApplyTransaction function

The Process function returns receipts and logs as well as the gas spent on the exchange. If the transaction fails due to insufficient gas, then an error will be returned.

func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) {
	var (
		receipts types.Receipts
		usedGas  = new(uint64)
		header   = block.Header()
		allLogs  []*types.Log
		gp       = new(GasPool).AddGas(block.GasLimit())
	)
	// Mutate the block and state according to any hard-fork specs
	if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
		misc.ApplyDAOHardFork(statedb)
	}
	// Iterate over and process the individual transactions
	for i, tx := range block.Transactions() {
		statedb.Prepare(tx.Hash(), block.Hash(), i)
		receipt, _, err := ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, usedGas, cfg)
		if err != nil {
			return nil, nil, 0, err
		}
		receipts = append(receipts, receipt)
		allLogs = append(allLogs, receipt.Logs...)
	}
	// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
	p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles())

	return receipts, allLogs, *usedGas, nil
}

Apply Transaction creates the EVM execution environment, applies the transaction in the state database, receipts the returned receipt, and creates a bloom filter

func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, uint64, error) {
	msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
	if err != nil {
		return nil, 0, err
	}
	// Create a new context to be used in the EVM environment
	context := NewEVMContext(msg, header, bc, author)
	// Create a new environment which holds all relevant information
	// about the transaction and calling mechanisms.
	vmenv := vm.NewEVM(context, statedb, config, cfg)
	// Apply the transaction to the current state (included in the env)
	_, gas, failed, err := ApplyMessage(vmenv, msg, gp)
	if err != nil {
		return nil, 0, err
	}
	// Update the state with pending changes
	var root []byte
	if config.IsByzantium(header.Number) {
		statedb.Finalise(true)
	} else {
		root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
	}
	*usedGas += gas

	// Create a new receipt for the transaction, storing the intermediate root and gas used by the tx
	// based on the eip phase, we're passing whether the root touch-delete accounts.
	receipt := types.NewReceipt(root, failed, *usedGas)
	receipt.TxHash = tx.Hash()
	receipt.GasUsed = gas
	// if the transaction created a contract, store the creation address in the receipt.
	if msg.To() == nil {
		receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
	}
	// Set the receipt logs and create a bloom for filtering
	receipt.Logs = statedb.GetLogs(tx.Hash())
	receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
	receipt.BlockHash = statedb.BlockHash()
	receipt.BlockNumber = header.Number
	receipt.TransactionIndex = uint(statedb.TxIndex())

	return receipt, gas, err
}

ApplyMessage creates the New StateTransition and calls the Transition Db () of the StateTransition.

func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) ([]byte, uint64, bool, error) {
	return NewStateTransition(evm, msg, gp).TransitionDb()
}

In TransitionDb, it will read whether the transaction is a contract creation transaction, call evm.Create if it is, and call evm.Call if it is not.

EVM contract creation process

The parameters required by the create function of EVM

  • ContractRef ContractRef is a reference to the contract's backing object
  • codeAndHash

    type codeAndHash struct {

    code []byte

    hash common.Hash

    }

  • gas
  • The value part wishes to transfer the value to the contract address when creating the contract
  • Addresses contract address remember to see how the contract address is generated

The main processes are as follows.

First, the contract address is calculated based on the caller's address and the nonce value of that address.

func CreateAddress(b common.Address, nonce uint64) common.Address {
	data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
	return common.BytesToAddress(Keccak256(data)[12:])
}

Then call the create function to create the contract

  1. Check whether the depth of contract execution has exceeded the depth of CallCreateDepth (1024) and returns empty if it exceeds.
  2. Check whether the sender's balance is sufficient for transfer, if not, return the error;
  3. The nonce value of the sender is added to 1.
  4. It does not exist before checking the current contract address.
  5. Snapshot current stateDB, if an error occurs, it can be returned according to the snapshot;
  6. According to the contract address, create an account in the state (that is, the contract address), and set the non CE value of the address to be 1;
  7. Transfer to that address (of course, some of the transactions creating the contract have a value of 0);
  8. Create a new contract to perform the current operation (the contract only works in the current context);
  9. Binding code for contract address is explained below.
  10. Execute in EVM and return results
  11. Check whether the code size exceeds the system settings (24576, byte units);
  12. If there is no error return, then calculate the gas needed to store code, and subtract the gas spent;
  13. If there are any errors, the snapshot will be used to return to the previous state of the world (of course, gas is still spent, the initial error is errExecution Reverted);

How does gas store code compute:

createDataGas := uint64(Contract Code byte Length) * params.CreateDataGas
CreateDataGas=200

The code is as follows

func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
	// Depth check execution. Fail if we're trying to execute above the
	// limit.
	if evm.depth > int(params.CallCreateDepth) {
		return nil, common.Address{}, gas, ErrDepth
	}
	if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
		return nil, common.Address{}, gas, ErrInsufficientBalance
	}

	//Add 1 to the nonce value of the caller
	nonce := evm.StateDB.GetNonce(caller.Address())
	evm.StateDB.SetNonce(caller.Address(), nonce+1)

	// Ensure there's no existing contract already at the designated address
	contractHash := evm.StateDB.GetCodeHash(address)
	if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
		return nil, common.Address{}, 0, ErrContractAddressCollision
	}
	// Create a snapshot, and restore status based on that snapshot when subsequent transactions fail
	snapshot := evm.StateDB.Snapshot()
	evm.StateDB.CreateAccount(address)
	if evm.chainRules.IsEIP158 {
		evm.StateDB.SetNonce(address, 1)
	}
	evm.Transfer(evm.StateDB, caller.Address(), address, value)

	//Create a temporary contract to execute content that only works in the current context
	contract := NewContract(caller, AccountRef(address), value, gas)
	contract.SetCodeOptionalHash(&address, codeAndHash)

	if evm.vmConfig.NoRecursion && evm.depth > 0 {
		return nil, address, gas, nil
	}

	if evm.vmConfig.Debug && evm.depth == 0 {
		evm.vmConfig.Tracer.CaptureStart(caller.Address(), address, true, codeAndHash.code, gas, value)
	}
	start := time.Now()

	//run is where the contract creation is actually performed in evm
	ret, err := run(evm, contract, nil, false)

	// check whether the max code size has been exceeded
	maxCodeSizeExceeded := evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize
	//Calculate the cost of storing contracts, 200 gas per byte
	if err == nil && !maxCodeSizeExceeded {
		createDataGas := uint64(len(ret)) * params.CreateDataGas
		if contract.UseGas(createDataGas) {
			evm.StateDB.SetCode(address, ret)
		} else {
			err = ErrCodeStoreOutOfGas
		}
	}

	//Any errors will be restored to the previous state according to snapshot, but gas costs will still be spent (except errExecution Reverted)
	if maxCodeSizeExceeded || (err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas)) {
		evm.StateDB.RevertToSnapshot(snapshot)
		if err != errExecutionReverted {
			contract.UseGas(contract.Gas)
		}
	}
	// Assign err if contract code size exceeds the max while the err is still empty.
	if maxCodeSizeExceeded && err == nil {
		err = errMaxCodeSizeExceeded
	}
	if evm.vmConfig.Debug && evm.depth == 0 {
		evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
	}
	return ret, address, contract.Gas, err

}

Execute contract calls
core/vm/interpreter.go fun(int *EVMInterpreter) Run(... ) 133 lines
Get the opcode, and then get the execution instructions from JumpTable according to the opcode. If the operation is illegal, the error will be returned.

op = contract.GetOp(pc)
operation := in.cfg.JumpTable[op]
if !operation.valid {
	return nil, fmt.Errorf("invalid opcode 0x%x", int(op))
}

The specific operation of GetOp is as follows

// GetOp returns the n'th element in the contract's byte array
func (c *Contract) GetOp(n uint64) OpCode {
	return OpCode(c.GetByte(n))
}

// GetByte returns the n'th byte in the contract's byte array
func (c *Contract) GetByte(n uint64) byte {
	if n < uint64(len(c.Code)) {
		return c.Code[n]
	}

	return 0
}

Operations are defined as follows:

type operation struct {
	execute     executionFunc  //The function that the operation executes concretely
	constantGas uint64         //constantGas cost for this operation
	dynamicGas  gasFunc			//This operation calculates dynamic dynamicGas function
	// minStack tells how many stack items are required
	minStack int
	// maxStack specifies the max length the stack can have for this operation
	// to not overflow the stack.
	maxStack int

	// memorySize returns the memory size required for the operation
	memorySize memorySizeFunc

	halts   bool // indicates whether the operation should halt further execution
	jumps   bool // indicates whether the program counter should not increment
	writes  bool // determines whether this a state modifying operation
	valid   bool // indication whether the retrieved operation is valid and known
	reverts bool // determines whether the operation reverts state (implicitly halts)
	returns bool // determines whether the operations sets the return data content
}

Compute new memory and extend memory to make it suitable for this operation

		var memorySize uint64
		if operation.memorySize != nil {
			memSize, overflow := operation.memorySize(stack)
			if overflow {
				return nil, errGasUintOverflow
			}
			//Memory expansion to 32 byte as a word, how memory overflow will return errors (gas calculation will also be extended later, that is, to calculate the multiple of word
			if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
				return nil, errGasUintOverflow
			}
		}

Each operation is executed through execute

res, err = operation.execute(&pc, in, contract, mem, stack)

The execute of each function shows the implementation of core/vm/instructions.go

The design of stack core/vm/stack.go is mainly the operation of push,pop,pek,dup,swap and other general stacks.
Memory's design core/vm/memory.go

Topics: snapshot Database