# 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("-----------------------")
```