# PoC1 : 구현(Implementation) ### Authors Jake Song(Onther Inc.) Carl Park(Onther Inc.) ____ Genesis 블록에 Stamina 컨트랙트를 넣어두고 내부적으로 컨트랙트 함수를 호출하여 가스 비용을 위임하는 것을 목적으로 하였다. 이를 위해 EVM 실행 함수를 사용하여 컨트랙트 실행 함수를 만들었고 tx-execution, tx-pool, state-transition 단계에 적용하였다. ## 1. go-ethereum 구현 ### 1.1 Stamina 컨트랙트 관련 parameter 설정 1. Stamina 컨트랙트 주소 및 bytecode 설정 ```go var StaminaContractAddressHex = "0x000000000000000000000000000000000000dead" var StaminaContractAddress = common.HexToAddress(StaminaContractAddressHex) // deployed bytecode var StaminaContractBin = "0x6080604052600436106100d0576000f..." ``` 2. Blockchain account 설정 * 내부적으로 Stamina 컨트랙트 함수 호출하기 위한 sender 설정 3. Stamina account 설정 * 내부적으로 Stamina 컨트랙트 함수 호출하기 위한 Stamina Contract account 설정 ```go type accountWrapper struct { address common.Address } func (a accountWrapper) Address() common.Address { return a.address } var BlockchainAccount = accountWrapper{common.HexToAddress("0x00")} var StaminaAccount = accountWrapper{StaminaContractAddress} var StaminaABI, _ = abi.JSON(strings.NewReader(contract.StaminaABI)) ``` ### 1.2 Stamina 컨트랙트 실행 함수 정의 컨트랙트 함수를 불러오기 위해 자체적으로 EVM을 실행하는 것을 정의 1. 거래를 보낸 sender의 delegatee를 조회하는 함수 ```go func GetDelegatee(evm *vm.EVM, from common.Address) (common.Address, error) { data, err := staminaCommon.StaminaABI.Pack("getDelegatee", from) if err != nil { return common.Address{}, err } ret, _, err := evm.StaticCall(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000) if err != nil { return common.Address{}, err } return common.BytesToAddress(ret), nil } ``` 2. delegatee의 Stamina를 조회하는 함수 ```go func GetStamina(evm *vm.EVM, delegatee common.Address) (*big.Int, error) { data, err := staminaCommon.StaminaABI.Pack("getStamina", delegatee) if err != nil { return big.NewInt(0), err } ret, _, err := evm.StaticCall(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000) if err != nil { return big.NewInt(0), err } stamina := new(big.Int) stamina.SetBytes(ret) return stamina, nil } ``` 3. delegatee의 Stamina를 증가시키는 함수 ```go func AddStamina(evm *vm.EVM, delegatee common.Address, gas *big.Int) error { data, err := staminaCommon.StaminaABI.Pack("addStamina", delegatee, gas) if err != nil { return err } _, _, err = evm.Call(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000, big.NewInt(0)) return err } ``` 4. delegatee의 Stamina를 감소시키는 함수 ```go func SubtractStamina(evm *vm.EVM, delegatee common.Address, gas *big.Int) error { data, err := staminaCommon.StaminaABI.Pack("subtractStamina", delegatee, gas) if err != nil { return err } _, _, err = evm.Call(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000, big.NewInt(0)) return err } ``` ### 1.3 tx-pool 검증 (static evm 정의) 1. tx-pool에 포함되기 위한 거래검증을 위해 임의의 static evm 정의 ```go // moscow - arbitrary msg & header & author func (pool *TxPool) newStaticEVM() *vm.EVM { msg := types.NewMessage( staminaCommon.BlockchainAccount.Address(), &staminaCommon.StaminaContractAddress, 0, big.NewInt(0), 1000000, big.NewInt(1e9), nil, false, ) vmConfig := vm.Config{} ctx := NewEVMContext( msg, &types.Header{ Number: big.NewInt(0), Time: big.NewInt(0), Difficulty: big.NewInt(0), }, pool.chain, &common.Address{}, ) return vm.NewEVM( ctx, pool.currentState, pool.chainconfig, vmConfig, ) } ``` ### 1.4 Genesis블록 환경 구성(Stamina 컨트랙트 구성) 1. Stamina 컨트랙트 바이트 코드를 Genesis블록에 삽입 ```go var err error staminaBinBytes, err := hex.DecodeString(staminaCommon.StaminaContractBin[2:]) if err != nil { panic(err) } staminaCommon.StaminaContractAddress: { Code: staminaBinBytes, Balance: big.NewInt(0), }, ``` ### 1.5 tx-execution / tx-pool / tx-validation 수정 1. state transition 적용 * Stamina로 가스 구입하기 ```go func (st *StateTransition) buyDelegateeGas(delegatee common.Address) error { mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice) balance, err := stamina.GetStamina(st.evm, delegatee) if err != nil { return err } if balance.Cmp(mgval) < 0 { return errInsufficientBalanceForGas } if err := st.gp.SubGas(st.msg.Gas()); err != nil { return err } st.gas += st.msg.Gas() st.initialGas = st.msg.Gas() stamina.SubtractStamina(st.evm, delegatee, mgval) return nil } ``` * nonce를 확인하여 유효한 거래인지 검증하고 유효하다면 buyDelegateeGas 실행 ```go func (st *StateTransition) preDelegateeCheck(delegatee common.Address) error { // Make sure this transaction's nonce is correct. if st.msg.CheckNonce() { nonce := st.state.GetNonce(st.msg.From()) if nonce < st.msg.Nonce() { return ErrNonceTooHigh } else if nonce > st.msg.Nonce() { return ErrNonceTooLow } } return st.buyDelegateeGas(delegatee) } ``` 2. state transition에 적용 * delegatee 있는지 확인 * delegatee가 있다면 delegatee가 위임하여 가스비용만큼의 stamina 부담 ```go // get delegatee delegatee, _ := stamina.GetDelegatee(evm, msg.From()) log.Info("GetDelegatee", "from", msg.From(), "delegatee", delegatee) // moscow - if has delegatee if delegatee != common.HexToAddress("0x00") { if err = st.preDelegateeCheck(delegatee); err != nil { return } // Pay intrinsic gas gas, err := IntrinsicGas(st.data, contractCreation, homestead) if err != nil { return nil, 0, false, err } if err = st.useGas(gas); err != nil { return nil, 0, false, err } if contractCreation { ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value) } else { // Increment the nonce for the next transaction st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value) } if vmerr != nil { log.Debug("VM returned with error", "err", vmerr) // The only possible consensus-error would be if there wasn't // sufficient balance to make the transfer happen. The first // balance transfer may never fail. if vmerr == vm.ErrInsufficientBalance { return nil, 0, false, vmerr } } st.refundDelegateeGas(delegatee) // TODO: gas fee to miner st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice)) return ret, st.gasUsed(), vmerr != nil, err } // moscow - original version if err = st.preCheck(); err != nil { return } ``` * remaining stamina가 있다면 delegatee에게 환불 ```go func (st *StateTransition) refundDelegateeGas(delegatee common.Address) { // Apply refund counter, capped to half of the used gas. refund := st.gasUsed() / 2 if refund > st.state.GetRefund() { refund = st.state.GetRefund() } st.gas += refund // Return ETH for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice) stamina.AddStamina(st.evm, delegatee, remaining) // Also return remaining gas to the block gas counter so it is // available for the next transaction. st.gp.AddGas(st.gas) } ``` 3. tx pool에 적용 * delegatee의 stamina, transaction sender의 balance가 충분한지 확인 ```go evm := pool.newStaticEVM() delegatee, err := stamina.GetDelegatee(evm, from) if err != nil { return ErrStaminaGetDelegatee } if delegatee != common.HexToAddress("0x00") { mgval := new(big.Int).Mul(tx.GasPrice(), big.NewInt(int64(tx.Gas()))) // delegatee should have enough stemina // cost == GP * GL if stamina, _ := stamina.GetStamina(evm, delegatee); stamina.Cmp(mgval) < 0 { return ErrInsufficientStamina } // sender should have enough value if pool.currentState.GetBalance(from).Cmp(tx.Value()) < 0 { return ErrInsufficientValue } } else { // Transactor should have enough funds to cover the costs // cost == V + GP * GL if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 { return ErrInsufficientFunds } } ``` * stamina와 balance 충분한 거래는 tx-pool에 포함하고 충분하지 않다면 tx-pool에서 제외 ```go // Drop all transactions that are too costly (low balance or out of gas) drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas) // moscow - do not drop tx if delegatee has enough stamina evm := pool.newStaticEVM() delegatee, _ := stamina.GetDelegatee(evm, addr) stamina, _ := stamina.GetStamina(evm, delegatee) balance := pool.currentState.GetBalance(addr) var costlimit *big.Int if stamina.Cmp(balance) >= 0 { costlimit = stamina } else { costlimit = balance } drops, _ := list.Filter(costlimit, pool.currentMaxGas) ``` ## 3.2 py-evm 구현 ### 3.2.1 py-evm 개요 및 구조 py-evm은 Ethereum의 하드포크에 따른 EVM을 정의하고 각 EVM을 상속하여 client를 구현하고 있다. Frontier: Homestead: Tangerine Whistle: Suprious Dragon: Byzantium: ### 3.2.2 구현 Byzantium EVM을 상속하는 Moscow EVM을 정의하여 가스비 위임 모델을 구현하였다. ### 3.2.3 Stamina 컨트랙트 실행함수 EVM 실행 함수 정의 EVM 실행 함수를 정의하고 Stamina 컨트랙트 함수를 실행하기 위한 함수를 정의함 1. EVM 실행함수 정의 * message 객체를 만든다 * transaction context를 만든다 * state, message, transaction context를 인자로 받아 VM을 실행한다 ```python def execute_bytecode(state, origin, gas_price, gas, to, sender, value, data, code, code_address=None, ): """ Execute raw bytecode in the context of the current state of the virtual machine. """ if origin is None: origin = sender # Construct a message message = Message( gas=gas, to=to, sender=sender, value=value, data=data, code=code, code_address=code_address, ) # Construction a tx context transaction_context = state.get_transaction_context_class()( gas_price=gas_price, origin=origin, ) # Execute it in the VM return state.get_computation(message, transaction_context).apply_computation( state, message, transaction_context, ) ``` 2. 컨트랙트 함수 호출하는 함수 정의 * delegatee가 있는 지 확인하는 함수 ```python def getDelegate(state, delegator) : fdata = keccak("getDelegate(address)".encode()) fnsig = fdata[0:4] adata = encode_single('address', delegator) data = fnsig + adata vm_state = state code = vm_state.account_db.get_code(STAMINA) # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None) computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None) assert(computation.is_success) addr = computation._memory._bytes return addr ``` * delegatee의 Stamina를 가져오는 함수 ```python def getStamina(state, delegate) : fdata = keccak("getStamina(address)".encode()) fnsig = fdata[0:4] adata = encode_single('address', delegate) data = fnsig + adata vm_state = state code = vm_state.account_db.get_code(STAMINA) # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None) computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None) assert(computation.is_success) ret = computation.output return ret ``` * Stamina를 증가시키는 함수 ```python def addStamina(state, delegate, val) : fdata = keccak("addStamina(address,uint256)".encode()) fnsig = fdata[0:4] adata = encode_single('(address,uint256)', [delegate,val]) data = fnsig + adata vm_state = state code = vm_state.account_db.get_code(STAMINA) # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None) computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None) assert(computation.is_success) ``` * Stamina를 감소시키는 함수 ```python def subtractStamina(state, delegate, val) : fdata = keccak("subtractStamina(address,uint256)".encode()) fnsig = fdata[0:4] adata = encode_single('(address,uint256)', [delegate,val]) data = fnsig + adata vm_state = state code = vm_state.account_db.get_code(STAMINA) # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None) computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None) assert(computation.is_success) ``` ### 3.2.3 state-transition 수정 1. Moscow VM이라는 Custom VM을 정의하여 수정 ### 3.2.4 Script 파일 작성 delegatee설정 -> deposit -> transfer 과정을 만들고 가스비가 위임되는 것을 시나리오 형태로 구현 1. Account 설정 ```python ADDRESS_1_PRIVATE_KEY = keys.PrivateKey( decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8') ) ADDRESS_2_PRIVATE_KEY = keys.PrivateKey( decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d0') ) ADDRESS_3_PRIVATE_KEY = keys.PrivateKey( decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d1') ) ADDRESS_4_PRIVATE_KEY = keys.PrivateKey( decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d2') ) ADDRESS_1 = Address(ADDRESS_1_PRIVATE_KEY.public_key.to_canonical_address()) ADDRESS_2 = Address(ADDRESS_2_PRIVATE_KEY.public_key.to_canonical_address()) ADDRESS_3 = Address(ADDRESS_3_PRIVATE_KEY.public_key.to_canonical_address()) ADDRESS_4 = Address(ADDRESS_4_PRIVATE_KEY.public_key.to_canonical_address()) BLOCKCHAIN = Address(b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0a') STAMINA = Address(b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02') ``` 2. Genesis 블록 설정 ```python GENESIS_STATE = { ADDRESS_1: { "balance" : 10**19, "nonce" : 0, "code" : b"", "storage" : {} }, ADDRESS_2: { "balance" : 10**19, "nonce" : 0, "code" : b"", "storage" : {} }, ADDRESS_3: { "balance" : 10**19, "nonce" : 0, "code" : b"", "storage" : {} }, ADDRESS_4: { "balance" : 10**19, "nonce" : 0, "code" : b"", "storage" : {} }, STAMINA: { "balance" : 10**19, "nonce" : 0, "code" : b'0x6080604052600436106100fc5...', "storage" : {} } } ``` 3. Chain 설정(Mining Chain, MemoryDB, Gensis Block) ```python klass = chains.base.MiningChain.configure( __name__='TestChain', vm_configuration=( (constants.GENESIS_BLOCK_NUMBER, ByzantiumVM), )) chain = klass.from_genesis(MemoryDB(), GENESIS_PARAMS, GENESIS_STATE) ``` 4. TX1 - Stamina 컨트랙트 init 함수 실행 * minDeposit(최소 예치금) 설정 ```python ######### TX1 ########################### # init vm = chain.get_vm() fdata = keccak("init(uint256)".encode()) fnsig = fdata[0:4] adata = encode_single('uint256', 1000000000000000000) data = fnsig + adata nonce = vm.state.account_db.get_nonce(ADDRESS_1) tx1 = vm.create_unsigned_transaction( nonce=nonce, gas_price=100, gas=100000, to=STAMINA, value=0, data=data, ) signed_tx1 = tx1.as_signed_transaction(ADDRESS_1_PRIVATE_KEY) new_header, receipt, computation = chain.apply_transaction(signed_tx1) # We have to finalize the block first in order to be able read the # attributes that are important for the PoW algorithm block = chain.get_vm().finalize_block(chain.get_block()) # based on mining_hash, block number and difficulty we can perform # the actual Proof of Work (PoW) mechanism to mine the correct # nonce and mix_hash for this block nonce, mix_hash = mine_pow_nonce( block.number, block.header.mining_hash, block.header.difficulty) block = chain.mine_block(mix_hash=mix_hash, nonce=nonce) vm = chain.get_vm() print("########### init (from: ADDRESS_1) ###############") print("BLOCK1 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1))) print("BLOCK1 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2))) print("BLOCK1 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3))) print("BLOCK1 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4))) print("-----------------------") ``` 5. TX2 - setDelegatee 실행 * delegatee가 delegator 지정 ```python ############ TX2 ################## # setDelegatee vm = chain.get_vm() fdata = keccak("setDelegatee(address)".encode()) fnsig = fdata[0:4] adata = encode_single('address', ADDRESS_2) data = fnsig + adata nonce = vm.state.account_db.get_nonce(ADDRESS_3) tx2 = vm.create_unsigned_transaction( nonce=nonce, gas_price=100, gas=100000, to=STAMINA, value=0, data=data, ) signed_tx2 = tx2.as_signed_transaction(ADDRESS_3_PRIVATE_KEY) _, _, computation = chain.apply_transaction(signed_tx2) # We have to finalize the block first in order to be able read the # attributes that are important for the PoW algorithm block = chain.get_vm().finalize_block(chain.get_block()) # based on mining_hash, block number and difficulty we can perform # the actual Proof of Work (PoW) mechanism to mine the correct # nonce and mix_hash for this block nonce, mix_hash = mine_pow_nonce( block.number, block.header.mining_hash, block.header.difficulty) block = chain.mine_block(mix_hash=mix_hash, nonce=nonce) vm = chain.get_vm() print("########### setDelegatee (delegate: ADDRESS_3, delegator: ADDRESS_2)###############") print("BLOCK2 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1))) print("BLOCK2 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2))) print("BLOCK2 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3))) print("BLOCK2 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4))) print("-----------------------") ``` 6. deposit 실행 * delegator가 delegatee에게 deposit함 ```python ############ TX3 ################## # deposit vm = chain.get_vm() fdata = keccak("deposit(address)".encode()) fnsig = fdata[0:4] adata = encode_single('address', ADDRESS_3) data = fnsig + adata nonce = vm.state.account_db.get_nonce(ADDRESS_2) tx3 = vm.create_unsigned_transaction( nonce=nonce, gas_price=100, gas=100000, to=STAMINA, value=1000000000000000000, data=data, ) signed_tx3 = tx3.as_signed_transaction(ADDRESS_2_PRIVATE_KEY) _, _, computation = chain.apply_transaction(signed_tx3) # We have to finalize the block first in order to be able read the # attributes that are important for the PoW algorithm block = chain.get_vm().finalize_block(chain.get_block()) # based on mining_hash, block number and difficulty we can perform # the actual Proof of Work (PoW) mechanism to mine the correct # nonce and mix_hash for this block nonce, mix_hash = mine_pow_nonce( block.number, block.header.mining_hash, block.header.difficulty) block = chain.mine_block(mix_hash=mix_hash, nonce=nonce) vm = chain.get_vm() print("########### deposit (delegate: ADDRESS_3, delegator: ADDRESS_2) ###############") print("BLOCK3 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1))) print("BLOCK3 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2))) print("BLOCK3 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3))) print("BLOCK3 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4))) print("-----------------------") ``` 7. transfer 실행 ```python ############ TX4 ################## # transfer vm = chain.get_vm() nonce = vm.state.account_db.get_nonce(ADDRESS_2) tx4 = vm.create_unsigned_transaction( nonce=nonce, gas_price=100, gas=100000, to=ADDRESS_4, value=1000000000000000000, data=data, ) signed_tx4 = tx4.as_signed_transaction(ADDRESS_2_PRIVATE_KEY) _, _, computation = chain.apply_transaction(signed_tx4) # We have to finalize the block first in order to be able read the # attributes that are important for the PoW algorithm block = chain.get_vm().finalize_block(chain.get_block()) # based on mining_hash, block number and difficulty we can perform # the actual Proof of Work (PoW) mechanism to mine the correct # nonce and mix_hash for this block nonce, mix_hash = mine_pow_nonce( block.number, block.header.mining_hash, block.header.difficulty) block = chain.mine_block(mix_hash=mix_hash, nonce=nonce) vm = chain.get_vm() print("########### transfer (from: ADDRESS_2, to: ADDRESS_4)###############") print("BLOCK4 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1))) print("BLOCK4 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2))) print("BLOCK4 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3))) print("BLOCK4 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4))) print("-----------------------") ```